Árvore de páginas

No exemplo abaixo, imagine que criamos uma função de Hashing "pesada" ... onde para determinar um Hash de uma determinada palavra, são necessárias mais de 256 mil calculos por palavra. E temos uma lista de palavras para calcular o "SuperHash". Usando tabelas temporárias compartilhadas, conseguimos criar uma fila para alguns jobs (a gosto do freguês ou da capacidade da máquina) para cada job processar uma palavra. 

/* Exemplo de criação de fila de processamento */ 

user function MyHash()
	Local cTable
	Local aStru := {}
	Local nI, nTimer
	Local nJobs := 3

	aadd(aStru,{"PALAVRA","C",20,0})
	aadd(aStru,{"JOBID","C",2,0})
	aadd(aStru,{"HEXHASH","C",40,0})
	aadd(aStru,{"TEMPO","N",12,3})
	
	tclink()

	DbCreate("%SHTMP%",aStru,"TOPCONN")
	cTable := tcconfig("GETTEMPNAME=%SHTMP%")

	// 7 palavras para processamento
	aPalavras := {}
	aadd(aPalavras,"ARQUIPELAGO")
	aadd(aPalavras,"AUTOMOVEL")
	aadd(aPalavras,"ONIBUS")
	aadd(aPalavras,"PARALELEPIPEDO")
	aadd(aPalavras,"BETERRABA")
	aadd(aPalavras,"PTERODACTILO")
	aadd(aPalavras,"CHEWBACCA")

	// abre a tabela
	USE (cTable) ALIAS TMP SHARED NEW VIA "TOPCONN"

	// acrescenta as palavras no arquivo
	aeval( aPalavras , { |x| dbappend(), TMP->PALAVRA := x , dbrunlock() })

	// Limpa variavel global de controle
	PutGlbValue("PROCJOBS","")

	// Inicia os jobs
	For nI := 1 to nJobs
		StartJob("U_JOBHASH",getenvserver(),.f.,strzero(nI,2),cTable)
		Sleep(100)
	Next

	// Inicia contagem de tempo
	nTimer := seconds()

	// Espera os jobs terminarem
	MsgRun("Calculando Hashes","Aguarde ... ",{|| WaitProcs(nJobs) } )

	// Mostra os dados processados
	// lendo a propria tabela

	DBGotop()
	While !eof()
		conout(TMP->PALAVRA+" => "+TMP->HEXHASH+' (JOB '+TMP->JOBID+') em '+str(TMP->TEMPO,12,3)+' s.')
		DbSkip()
	Enddo

	// Fecha a tabela
	DbCloseArea()
	
	// Apaga a tabela
	TcDelFile(cTable)

	MsgInfo("Tempo Total : "+str(seconds()-nTimer,12,3)+" s.","Atenção")

return

// Espera o processamento estar temrinado
// Usa uma variavel global em memoria para controle 
STATIC Function WaitPRocs(nJobs)

	Local cOk := replicate(".",nJobs)
	While !killapp() .and. GetGlbValue("PROCJOBS") != cOk
		Sleep(250)
	Enddo

RETURN


// Job de processamento 
// Recebe o nome da tabela, processa os dados pendentes 
// Ao temrinar , acrescenta um "." na global de controle

USER Function JOBHASH(cIdJob,cTable)
	Local cPalavra, cJobRun
	Local cRet
	Local nTimer

	tclink()

	USE (cTable) ALIAS TMP SHARED NEW VIA "TOPCONN"

	While !Eof()

		cPalavra := Alltrim(TMP->PALAVRA)
		cJobRun := TMP->JOBID

		IF dbrlock(recno())

			// Se bloqueou , pode processsar
			// mas verifica se já nao está processado ...

			If !empty(TMP->HEXHASH)
				conout("Job ["+cIdJob+"] ["+cPalavra+"] foi processada pelo Job ["+cJobRun+"]")
				dbrunlock()
				dbskip()
				LOOP
			Endif

			conout("Job ["+cIdJob+"] ["+cPalavra+"] sendo calculado ... aguarde ...")

			// Atualiza o JobId que está processando esse registro 
			// e faz flush da informação pro banco de dados 
			TMP->JOBID := cIdJob
			DBCommit()

			nTimer := seconds()
			cRet := SuperHash(cPalavra)
			conout("Job ["+cIdJob+"] ["+cPalavra+"] calculado em "+str(seconds()-nTimer,12,3)+" s.")

			// atualiza no registro o hash calculado
			// e o tempo que demorou 
			TMP->HEXHASH := cRet
			TMP->TEMPO := round(seconds()-nTimer,3)

			// e Solta o bloqueio
			Dbrunlock()

		else

			// Nao bloqueou , esta sendo processada por outro job
			conout("Job ["+cIdJob+"] ["+cPalavra+"] ignorada -- em processamento pelo Job ["+cJobRun+"] ")

		Endif

		// Vai para o proximo
		DbSkip()

	ENDDO

	PutGlbValue("PROCJOBS",GetGlbValue("PROCJOBS")+'.')

	DbCloseArea()

RETURN

// Aplica SHA1 262144 para cada palavra 
// retorna SHA1 no formato hexadecimal ( 40 caracteres ) 

STATIC function SuperHash(cPalavra)
	Local nI
	For nI := 1 to 256 * 1024
		cPalavra := SHA1(cPalavra,2)
	Next
RETURN cPalavra




A sacada do exemplo está na utilização da tabela criada. Cada job recebe apenas um identificador para si mesmo, e o nome da tabela compartilhada que possui os dados a serem processados. Na tabela existe um campo de controle para informar qual job que pegou cada palavra. Todos os jobs abrem a tabela na ordem natural de inserção de registros, e cada job tenta bloquear o registro atualmente posicionado. Se o JOB consegue bloquear, ele verifica – após conseguir o bloqueio – se aquela palavra já tem a informação de Hash. Se já estiver preenchida, esse job chegou "atrasado" e alguém já processou aquela palavra, e ele solta o bloqueio e pula para a próxima. Se ainda não tem o hash calculado, ele mantém o bloqueio durante o processamento, e antes de começar a processar, atualiza a informação de que ele pegou a palavra daquele registro. Quando o job termina, ele grava o hash calculado – pode demorar mais de 3 segundos cada cálculo... E ao terminar um cálculo, cada job verifica o próximo registro. E, se o registro está bloqueado, o job informa que ignorou a palavra, pois está sendo processada por outro job, e informa qual job que é – que foi atualizado na tabela.

Durante o processamento, usamos uma variável global atualizada pelas threads no final do processamento, onde cada JOB que chega ao final da tabela temporária compartilhada acrescenta um "." na string da global ... enquanto o processo que disparou os jobs fica no ar até que a global seja igual a ".." ( para dois jobs, por exemplo). Desse modo, eu não preciso dividir as palavras para cada job ... cada um pega a proxima palavra livre e não calculada. 

Assim, ao colocarmos por exemplo dois jobs para processar, podemos ter um resultado assim no log de console do AppServer:

Job [01] [ARQUIPELAGO] sendo calculado ... aguarde ...
Job [02] [ARQUIPELAGO] ignorada -- em processamento pelo Job [01]
Job [02] [AUTOMOVEL] sendo calculado ... aguarde ...
Job [01] [ARQUIPELAGO] calculado em        2.817 s.
Job [01] [AUTOMOVEL] ignorada -- em processamento pelo Job [02]
Job [01] [ONIBUS] sendo calculado ... aguarde ...
Job [02] [AUTOMOVEL] calculado em        3.119 s.
Job [02] [ONIBUS] ignorada -- em processamento pelo Job [01]
Job [02] [PARALELEPIPEDO] sendo calculado ... aguarde ...
Job [01] [ONIBUS] calculado em        3.257 s.
Job [01] [PARALELEPIPEDO] ignorada -- em processamento pelo Job [02]
Job [01] [BETERRABA] sendo calculado ... aguarde ...
Job [02] [PARALELEPIPEDO] calculado em        2.997 s.
Job [02] [BETERRABA] ignorada -- em processamento pelo Job [01]
Job [02] [PTERODACTILO] sendo calculado ... aguarde ...
Job [01] [BETERRABA] calculado em        3.466 s.
Job [01] [PTERODACTILO] ignorada -- em processamento pelo Job [02]
Job [01] [CHEWBACCA] sendo calculado ... aguarde ...
Job [02] [PTERODACTILO] calculado em        3.466 s.
Job [02] [CHEWBACCA] ignorada -- em processamento pelo Job [01]
Job [01] [CHEWBACCA] calculado em        2.809 s.

ARQUIPELAGO          => 53be976968beeb949b9e17515b388ff1be51c582 (JOB 01) em        2.817 s.
AUTOMOVEL            => abb157df53f6b89befdb93084b3855c9b89752b2 (JOB 02) em        3.119 s.
ONIBUS               => 9ca3cf8e0d8030ffd85a62782ccafd047cf80bca (JOB 01) em        3.257 s.
PARALELEPIPEDO       => 797525c9c858a969417569ff9dc309a9a7b245ad (JOB 02) em        2.997 s.
BETERRABA            => 667320efb1e4194f125b2139448a04d897bf9f8d (JOB 01) em        3.466 s.
PTERODACTILO         => 4891a5682a28f4472531a05387d51bd43cdb7874 (JOB 02) em        3.466 s.
CHEWBACCA            => 8122c6c9720adb5971b57468766ce32efdb71140 (JOB 01) em        2.809 s.

Agora , ao colocarmos 3 jobs processando, podemos ter um resultado assim:

Job [01] [ARQUIPELAGO] sendo calculado ... aguarde ...
Job [02] [ARQUIPELAGO] ignorada -- em processamento pelo Job [01]
Job [02] [AUTOMOVEL] sendo calculado ... aguarde ...
Job [03] [ARQUIPELAGO] ignorada -- em processamento pelo Job [01]
Job [03] [AUTOMOVEL] ignorada -- em processamento pelo Job [02]
Job [03] [ONIBUS] sendo calculado ... aguarde ...
Job [01] [ARQUIPELAGO] calculado em        3.169 s.
Job [01] [AUTOMOVEL] ignorada -- em processamento pelo Job [02]
Job [01] [ONIBUS] ignorada -- em processamento pelo Job [03]
Job [01] [PARALELEPIPEDO] sendo calculado ... aguarde ...
Job [03] [ONIBUS] calculado em        3.161 s.
Job [03] [PARALELEPIPEDO] ignorada -- em processamento pelo Job [01]
Job [03] [BETERRABA] sendo calculado ... aguarde ...
Job [02] [AUTOMOVEL] calculado em        3.321 s.
Job [02] [ONIBUS] foi processada pelo Job [03]   <===== REPARE AQUI 
Job [02] [PARALELEPIPEDO] ignorada -- em processamento pelo Job [01]
Job [02] [BETERRABA] ignorada -- em processamento pelo Job [03]
Job [02] [PTERODACTILO] sendo calculado ... aguarde ...
Job [01] [PARALELEPIPEDO] calculado em        3.413 s.
Job [01] [BETERRABA] ignorada -- em processamento pelo Job [03]
Job [01] [PTERODACTILO] ignorada -- em processamento pelo Job [02]
Job [01] [CHEWBACCA] sendo calculado ... aguarde ...
Job [03] [BETERRABA] calculado em        3.405 s.
Job [03] [PTERODACTILO] ignorada -- em processamento pelo Job [02]
Job [03] [CHEWBACCA] ignorada -- em processamento pelo Job [01]
Job [02] [PTERODACTILO] calculado em        3.411 s.
Job [02] [CHEWBACCA] ignorada -- em processamento pelo Job [01]
Job [01] [CHEWBACCA] calculado em        2.813 s.

ARQUIPELAGO          => 53be976968beeb949b9e17515b388ff1be51c582 (JOB 01) em        3.169 s.
AUTOMOVEL            => abb157df53f6b89befdb93084b3855c9b89752b2 (JOB 02) em        3.321 s.
ONIBUS               => 9ca3cf8e0d8030ffd85a62782ccafd047cf80bca (JOB 03) em        3.161 s.
PARALELEPIPEDO       => 797525c9c858a969417569ff9dc309a9a7b245ad (JOB 01) em        3.413 s.
BETERRABA            => 667320efb1e4194f125b2139448a04d897bf9f8d (JOB 03) em        3.405 s.
PTERODACTILO         => 4891a5682a28f4472531a05387d51bd43cdb7874 (JOB 02) em        3.411 s.
CHEWBACCA            => 8122c6c9720adb5971b57468766ce32efdb71140 (JOB 01) em        2.813 s.

Reparem que o JOB 01 pegoua primeira palavra - ARQUIPELAGO, e começou a calcular ... O Job 2 ignorou ARQUIPELAGO pois está em processamento pelo JOB 01, e acabou pegando a segunda palavra – AUTOMOVEL, e começou a calcular. O Job 03, que chegou depois, ignorou ARQUIPELADO e AUTOMOVEL, que estão sendo processadas por outros jobs, e acabou pegando a 3a palavra - ONIBUS, e comecou a processar. Enquanto o JOB 03 processava a 3a palavra, o JOB 01 terminou de processar ARQUIPELAGO, ignorou AUTOMOVEL e ONIBUS ( que estão em processamento pelos jobs 2 e 3 respectivamente), e comecou a calcular a 4a palavra PARALELEPIPEDO. O Job 03, mesmo pegando a 3a palavra, depois do JOB 02, terminou de processar ONIBUS antes do Job 02 processar AUTOMOVEL. Ao terminar o ONIBUS, o Job 03 ignorou a 4a palavra PARALELEPIPEDO, que está sendo calculada pelo Job 01, e começou a calcular BETERRABA, que é a 5a palavra. Só depois disso que o Job 2, que processava a segunda palavra – AUTOMOVEL – terminou de calcular ... assim que o JOB 02 foi para a proxima palavra, ela não estava bloqueada, pois ela já foi processada. Ao conseguir o bloqueio do registro, e verificar que ela já foi processada, ele ignorou a palavra e foi para a próxima. 

Neste exemplo, a tabela temporária foi utilizada ao mesmo tempo para servir como controlador de fila e armazenar os resultados de cada processo, com controle de concorrência para apenas um job pegar uma palavra da tabela por vez, e você não se preocupar com o numero de jobs. 

Vale lembrar que este é um exemplo didático, os programas de exemplo não estão tratando eventuais falhas de processamento ( como por exemplo se a conexão com o DBAccess feita pela TCLInk foi estabelecida com sucesso, se houve falha na abertura da tabela, na deleção, etc ). E, que essa abordagem é eficas quando já subimos um numero determinado e pequeno de jobs. Fatalmente todos os Jobs vão ler todos os registros da tabela, e tentar bloquear um a um, mas mesmo assim o periodo de leitura e retorno imediato da tentativa de bloqueio, fazem com que todos os jobs processem cada um uma palavra ao mesmo tempo que os demais.

Com dois jobs, cada um processa uma palavra a cada 3 ou 4 segundos. Com dois jobs processando as palavras, o JOB 01 processou 4 palavras , ao mesmo tempo que o Job2 processou 3 palavras, isso demorou de 14 a 15 segundos para terminar. Usando 3 Jobs, um job processou 3 palavras, enquanto outros 2 jobs processaram 2 palavras cada, e o tempo caiu para 10 segundos. Vale lembrar que, como os processos do exemplo tem um alto consumo de CPU, colocar um numero de Jobs igual ao numero de cores / processadores da máquina deve consumir 100% de cpu de todos os cores durante o processamento. 






  • Sem rótulos