7 de outubro de 2009

Trabalhando com Threads em Delphi - Sincronização com Eventos - parte 2

Para mostrar um exemplo prático dos conceitos de que falei na primeira parte da sincronização de threads com eventos, vou trabalhar com o seguinte cenário : um programa que tenha 4 listas distintas com uma quantidade aleatório de items a serem ordenados. Cada lista será montada e ordenada em uma thread separada, simulando que as listas vieram de fontes diferentes. Como elas têm uma quantidade diferente de itens, haverá uma thread especial, desenhada apenas para aguardar o términio das demais. O efeito disso é que as threads estarão sincronizadas e o programa poderá prosseguir com sua execução nesse ponto com as 4 listas ordenadas. Veja novamente o esquema geral para esse cenário.
Threads com eventos

Para implementar essa solução em Delphi, eu comecei criando uma unit que centralizará o controle da sincronização. Ela terá uma variável global para representar o evento "Término das threads de Classificação" e funções para incrementar e decrementar a quantidade de Threads ativas. O objetivo é que cada nova thread de classificação que for criada incremente um contador interno e, quando essa Thread terminar, o contador seja decrementado. Quando ele atingir o valor "zero", o evento será acionado para avisar que as listas já estão ordenadas:
var _QtThreads : Integer;
_CS_QtThreads : TCriticalSection;

procedure IncQtThreads;
begin
{ Incrementa o contador global de threads. Usa a seção crítica para proteção já que várias threads podem tentar o acesso simultaneamente }
_CS_QtThreads.Acquire;

try // Por segurança, trata exceções
Inc(_QtThreads);
finally
_CS_QtThreads.Release;
end;
end;

procedure DecQtThreads;
begin
{ Decrementa o contador global de threads. Usa a seção crítica para proteção já que várias threads podem tentar o acesso simultaneamente }
_CS_QtThreads.Acquire;

try
Dec(_QtThreads);

{ Se Chegou a zero, significa que as threads de classificação já terminaram a execução. Então, notifica a ocorrência do evento p/ quem estiver aguardando }
if _QtThreads = 0 then
evtThreadsSort.SetEvent;
finally
_CS_QtThreads.Release;
end;
end;

A linha evtThreadsSort.SetEvent notifica que o evento esperado ocorreu. Então, na classe que representa a Thread aguardando, uso a variável do evento para esperar pela notificação:
procedure TWThrEsperaSort.Execute;
var res : TWaitResult;
begin
{ Espera no máximo 1 minuto e meio pelo evento }
res := evtThreadsSort.WaitFor(90000);
{ Se tudo correu bem, neste ponto as listas já estão ordenadas. }

Este código suspende a execução da Thread enquanto aguarda. É por essa razão que eu não uso a Thread principal para aguardar; se fizesse isso, o usuário perderia a interação com o Form durante a espera, dando a impressão que o programa todo "travou". Essa Thread só continuará sua execução quando o evento esperado ocorrer ou se o tempo de espera (timeout) expirar ou ainda se houver algum erro inesperado.

Se tudo correu bem, as listas estarão classificadas no ponto do código imediatamente após a chamada ao WaitFor e o processo que depende da classificação de todas as listas pode continuar.

Eu criei uma classe de Thread para realizar a classificação de listas e, no construtor dela, faço o incremento do contador de threads ativas. O decremento desse valor é chamado na função de finalização.
Constructor TWThrSort.Create (PStatus: TWThreadStatus);
begin
inherited Create (true);
IncQtThreads;
FreeOnTerminate := true;
OnTerminate := OnThreadFinish;
{ ... }
end;

procedure TWThrSort.OnThreadFinish(Sender: TObject);
begin
{ ... }
DecQtThreads;
{ ... }

Duas coisas a observar no trecho acima. Quando chamo o construtor herdado Create, informo True no parâmetro createSuspended, o que cria a Thread em estado suspenso. Isto significa que terei que iniciá-la manualmente. O outro ponto é que ajustei a propriedade FreeOnTerminate para True, indicando que a finalização ocorrerá automaticamente quando encerrar a função Execute.

A ligação disso tudo se dá no ponto em que as Threads de classificação são instanciadas. Neste caso, inclui o código abaixo como resposta ao clique de um botão no meu Form:
{ Reseta o evento para garantir que está no estado inicial }
evtThreadsSort.ResetEvent;

{ Cria a thread de espera passando o handle desse Form }
TWThrEsperaSort.Create(Handle);

{ Cria as threads de classificação }
for i := 0 to 3 do begin
lStatus := TWThreadStatus.Create;
{ ... }
thds[i] := TWThrSort.Create(lStatus);
end;
{ sincroniza o início das threads de classificação }
for i := 0 to 3 do
thds[i].Resume;
{ ... }

É preciso colocar a variável do Evento em seu estado inicial (ResetEvent) antes de criar a thread de espera e só então podemos criar as demais threads, dedicadas à classificação das listas.

Mas, porque iniciar as Threads manualmente neste caso? Se eu deixar a criação da Thread iniciá-la automaticamente e a primeira Thread for rápida o suficiente, ela poderia terminar antes da segunda Thread ter tempo de incrementar o contador, o que, por sua vez, acionaria o Evento antes da hora pois o contador das threads ativas voltaria ao valor zero. Ao fazer a criação e a execução em pontos distintos, o contador de threads ativas estará em seu valor máximo quando as threads se iniciarem. Assim, mesmo que a primeira Thread execute muito rápido, o contador só voltará ao valor zero quando todas terminarem, acionando o Evento no momento apropriado.

As classes TEvent e TCriticalSection encapsulam chamadas a funções da API do Windows para trabalhar com sincronização. Essas funções podem ser chamadas diretamente, se for necessário. A documentação para elas pode ser encontrada no site MSDN.

Para fazer o download código fonte do exemplo em Delphi 2005, clique aqui.

Nenhum comentário :

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.