quinta-feira, 16 de dezembro de 2010

Sinalizando uma Thread com TEvent

Olá!

Quando se programa uma thread existe uma questão importante a resolver que é como sinalizar a esta thread que há um processamento a ser feito.

Um bom exemplo disto é aquela thread que encapsula uma pilha de objetos que é alimentada por outra thread, logo ela só "precisa trabalhar" se houver objetos na pilha.

De cara, o código fica como no exemplo abaixo:

procedure TExemploEvent.Execute;
begin
  inherited;
  while not (Self.Terminated) do
  begin
   {$REGION 'Processamento da pilha'}
   //Não é sempre que haverá elementos na pilha
   if (Self.FQueue.Count > 0) then
   begin
     //Processamento da pilha
   end;
   {$ENDREGION}
  end;
end;

O efeito colateral disto é um consumo intenso de CPU, o que obviamente é péssimo. A solução então é colocar um Sleep(10) - evite o Application.ProcessMessages dentro de uma thread. Com isso o código fica assim:

procedure TExemploEvent.Execute;
begin
  inherited;
  while not (Self.Terminated) do
  begin
    //Sleep para aliviar a thread
    Sleep(10);

   {$REGION 'Processamento da pilha'}
   //Não é sempre que haverá elementos na pilha
   if (Self.FQueue.Count > 0) then
   begin
     //Processamento da pilha
   end;
   {$ENDREGION}
  end;
end;

Isso resolve o problema mas cria um desconforto: A thread fica em um loop insano e o Sleep(10) resolve parte do problema. Podemos nos contentar com isso ou fazer algo melhor.

Para isto podemos usar o TEvent, que será usado para sinalizar a Thread que há elementos na fila. Veja como fica o Execute neste novo contexto:

procedure TExemploEvent.Execute;
var
eEvent : TWaitResult;
begin
  inherited;
  while not (Self.Terminated) do
  begin
    //Esperando sinalização do evento
    eEvent := Self.FEvent.WaitFor(INFINITE);

    case eEvent of
      wrSignaled:
      begin
       {$REGION 'Processamento da pilha'}
       //Não é sempre que haverá elementos na pilha
       if (Self.FQueue.Count > 0) then
       begin
         //Processamento da pilha
       end;
       {$ENDREGION}
      end;

    //...

    end;
end;

Por fim, veja o exemplo completo.

unit Thread;

interface

uses
  Classes, Contnrs, SyncObjs;

type

  TExemploEvent = class(TThread)
  private
    FQueue: TObjectQueue;
    FEvent: TEvent;
    FCritical: TCriticalSection;
  public
    procedure AfterConstruction; override;
    procedure BeforeDesttruction; override;
    procedure Execute; override;
    procedure AdicionarItem(poItem: TObject);
  end;

implementation

uses
  SysUtils;

{ TExemploEvent }

//É através deste método que outras threads colocarão objetos na pilha
procedure TExemploEvent.AdicionarItem(poItem: TObject);
begin
  //Entra na seção crítica
  Self.FCritical.Enter;
  try
    //Coloca o item na pilha
    Self.FQueue.Push(poItem);

    //Sinaliza o TEvent
    Self.FEvent.SetEvent;
  finally
    //Sai da seção crítica
    Self.FCritical.Release;
  end;
end;

procedure TExemploEvent.AfterConstruction;
begin
  inherited;
  //Seção crítica para acessar a fila de objetos
  Self.FCritical := TCriticalSection.Create;

  //Fila de objetos que serão processados
  Self.FQueue    := TObjectQueue.Create;

  //Sinalizador
  Self.FEvent    := TEvent.Create(nil,False,True,'_exemploevent');
  //                              --- ----- ----  -------------
  //                               |    |     |         |
  //                               |    |     |         \............> Nome único para a instância, do contrário, será usado o já existente
  //                               |    |     \......................> Podemos já criar SINALIZADO
  //                               |    \............................> Indica se será resetado MANUALMENTE ou AUTOMATICAMENTE
  //                               \.................................> Atributos, nil basta na maioria das necessidades
end;

procedure TExemploEvent.BeforeDesttruction;
begin
  inherited;
  Self.FCritical.Free;
  Self.FQueue.Free;
  Self.FEvent.Free;
end;

procedure TExemploEvent.Execute;
var
eEvent : TWaitResult;
begin
  inherited;
  while not (Self.Terminated) do
  begin
    //Esperando sinalização do evento
    eEvent := Self.FEvent.WaitFor(INFINITE);

    case eEvent of
      //Processa a pilha somente se o evento foi sinalizado
      wrSignaled:
      begin
       {$REGION 'Processamento da pilha'}
       //Não é sempre que haverá elementos na pilha
       if (Self.FQueue.Count > 0) then
       begin
         //Entra na seção crítica
         Self.FCritical.Enter;
         try
           while (Self.FQueue.Count > 0) do
           begin
             Sleep(10);
             //Para efeitos de exemplificação, esta apenas livrando o objeto
             Self.FQueue.Pop.Free;
           end;
         finally
           //Sai da seção crítica
           Self.FCritical.Release;
         end;
       end;
       {$ENDREGION}
      end;

      wrTimeout:
      begin
        //Excedeu o tempo de espera
        Continue;
      end;

      wrAbandoned:
      begin
        Abort;
      end;

      wrError:
      begin
        Abort;
      end;

      wrIOCompletion:
      begin
        Abort;
      end;
    end;
  end;
end;

end.

E é isso ai.

5 comentários:

AgIlIzA disse...

Nesse exemplo, a thread efetuaria algum processamento logo depois que vc adicionasse algum item?

José Mário Silva Guedes disse...

Olá. Sim, é exatamente este o objetivo.

No exemplo que eu dei não estou fazendo nada útil, apenas liberado o objeto:

Self.FQueue.Pop.Free;

Rafael Bernstorff Biz disse...

Muito bom, exemplo simples e prático. Eu só adicionaria uma pequena correção. Na procedure TExemploEvent.Execute a sessão crítica deveria circundar somente as funções Self.FQueue.Count e Self.FQueue.Pop. As demais chamadas deverão ser realizadas fora da sessão crítica para evitar o congelamento da thread que chamar TExemploEvent.AdicionarItem.

Guill disse...

Minha Thread está se comportando mal. No Sleep ela não só espera, mas trava aplicação. (PS: não estou usando o Synchronize, obviamente) Alguma dica?

José Mário Silva Guedes disse...

Olá Guill, desculpe só responder agora, mil anos depois.

Provavelmente você já resolveu o problema mas queria registrar minha opinião.

O Sleep() não é thread-safe, ela realmente congela o processo como um todo.

Se você tem a necessidade de pausar um thread o correto é usar o TEvent porém tirando vantagem do timeout e não da sinalização em si.

Minha lista de blogs