Menu

Tradução

Português English Español Français

4 Formas de Comunicação entre Threads em Delphi: Métodos e Exemplos Práticos

Neste post, exploraremos os métodos para comunicação segura entre threads em Delphi, apresentando exemplos práticos que ilustram essas técnicas.

 

Comunicação entre Threads em Delphi
Comunicação

Introdução

Nesse post iremos abordar 4 Formas de Comunicação entre Threads em Delphi.

A programação multithreaded é uma técnica poderosa que permite a execução simultânea de tarefas, melhorando a eficiência e a responsividade das aplicações. No entanto, a comunicação entre threads pode ser complexa, especialmente quando se trata de garantir a segurança dos dados. Neste artigo, exploraremos os métodos para comunicação segura entre threads em Delphi, apresentando exemplos práticos que ilustram essas técnicas.

Importância da Comunicação entre Threads

Em aplicações que utilizam múltiplas threads, a comunicação é essencial para que as threads possam compartilhar dados e coordenar suas atividades. Um gerenciamento adequado da comunicação ajuda a evitar condições de corrida, deadlocks e corrupção de dados.

Métodos para Comunicação Segura entre Threads

Existem várias abordagens para permitir que threads se comuniquem de forma segura:

  1. Eventos: Utilizar eventos para notificar outras threads sobre a ocorrência de uma ação.
  2. Mensagens: Enviar mensagens entre threads utilizando a função PostMessage.
  3. Recursos Compartilhados com Sincronização: Utilizar seções críticas (TCriticalSection) para proteger o acesso a dados compartilhados.
  4. Queues (Filas): Implementar filas para gerenciar tarefas e dados entre threads de maneira ordenada.

Vamos analisar cada um desses métodos em detalhes, com exemplos.

1. Utilizando Eventos

Os eventos são uma maneira eficiente de notificar uma thread sobre a conclusão de uma tarefa ou a disponibilidade de dados.

Passo 1: Definir a Classe da Thread

Delphi

uses
  System.Classes, SyncObjs;

type
  TMyThread1 = class(TThread)
  private
    FEvent: TEvent;
    FData: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;
    property Data: Integer read FData;
  end;

constructor TMyThread1.Create;
begin
  inherited Create(True); // Cria a thread suspensa
  FEvent := TEvent.Create(nil, False, False, ''); // Inicializa o evento
end;

destructor TMyThread1.Destroy;
begin
  if Assigned(FEvent) then
    FEvent.Free; //Finaliza o evento
  inherited;
end;

procedure TMyThread1.Execute;
begin
  Sleep(1000); // Simula trabalho
  FData := 42; // Define um valor
  FEvent.SetEvent; // Sinaliza que o trabalho foi concluído
end;
  

Passo 2: Utilizar a Classe da Thread

Delphi

// Para usar a thread:
var
  MyThread: TMyThread1;
begin
  MyThread := TMyThread1.Create;//Cria a Thread
  try
    MyThread.Start;//Inicia a Thread
    MyThread.FEvent.WaitFor(INFINITE); // Aguarda o evento
    ShowMessage('Dado recebido: ' + IntToStr(MyThread.Data)); //Exibe a mensagem
  finally
    MyThread.Free;
  end;
end;
  

Explicação do Código

  1. Inclusão da Unidade SyncObjs:
    • uses System.Classes, SyncObjs;: Adiciona a unidade SyncObjs que contém a declaração da classe TEvent.
  2. Definição da Classe TMyThread1:
    • TMyThread1 = class(TThread): Declara uma nova classe de thread personalizada que herda de TThread.
    • FEvent: TEvent;: Declara uma variável de instância do tipo TEvent para gerenciamento de eventos de sincronização.
    • FData: Integer;: Declara uma variável de instância para armazenar dados processados pela thread.
  3. Construtor da Classe TMyThread1:
    • constructor Create;: Inicializa uma nova instância de TMyThread1.
    • inherited Create(True);: Chama o construtor da classe base TThread com o parâmetro True, que cria a thread em estado suspenso.
    • FEvent := TEvent.Create;: Cria uma nova instância de TEvent.
  4. Destrutor da Classe TMyThread1:
    • destructor Destroy; override;: Sobrescreve o destrutor para liberar os recursos da thread.
    • FEvent.Free;: Libera a instância de TEvent.
    • inherited;: Chama o destrutor da classe base.
  5. Método Execute:
    • procedure Execute; override;: Sobrescreve o método Execute para definir o código que será executado pela thread.
    • FEvent.WaitFor(INFINITE);: Aguarda indefinidamente até que o evento seja sinalizado.
    • FData := 42;: Simula o processamento atribuindo um valor à variável FData.
  6. Utilização da Thread aguardando o evento:
    • var MyThread: TMyThread1;: Cria a variavel do tipo TMyThread1.
    • MyThread := TMyThread1.Create;: Cria a instância da Thread.
    • MyThread.Start; : Inicia a Thread.
    • MyThread.FEvent.WaitFor(INFINITE);: Aguarda o evento até que ele seja concluído.
    • ShowMessage('Dado recebido: ' + IntToStr(MyThread.Data));: Exibe a mensagem com o dado sincronizado.
    • MyThread.Free;: Libera a instância da Thread.

Veja abaixo a ilustração do projeto utilizando eventos:

Ilustração de Eventos.


2. Utilizando Mensagens

Vamos criar uma aplicação Delphi que utiliza threads para realizar uma tarefa em segundo plano e envia mensagens para a interface gráfica utilizando PostMessage. A função PostMessage permite enviar mensagens entre threads de forma assíncrona. A tarefa da thread será simplesmente esperar alguns segundos e depois enviar uma mensagem para a interface gráfica indicando que a tarefa foi concluída.

Passo 1: Definir Constante de Mensagem

Delphi


const
  WM_UPDATE = WM_USER + 1;
  

Passo 2: Definir a Classe da Thread

Delphi

type
  TMyThread2 = class(TThread)
  private
    FHandle: HWND;
  protected
    procedure Execute; override;
  public
    constructor Create(AHandle: HWND);
  end;

constructor TMyThread2.Create(AHandle: HWND);
begin
  inherited Create(False);
  FreeOnTerminate := True; //Finaliza assim que terminar de Executar
  FHandle := AHandle;
end;

procedure TMyThread2.Execute;
begin
  // Simular uma operação de longa duração
  Sleep(3000);
  // Enviar mensagem para a interface gráfica
  PostMessage(FHandle, WM_UPDATE, 0, 0);
end;
  

Passo 3: Sobrescrever o Método WndProc no Formulário

Delphi

procedure TForm1.WndProc(var Msg: TMessage);
begin
  inherited WndProc(Msg);
  if Msg.Msg = WM_UPDATE then
    ShowMessage('Mensagem recebida de outra thread!');
end;
  

Passo 4: Iniciar a Thread a partir do Formulário

Delphi

procedure TForm1.Button1Click(Sender: TObject);
begin
  // Iniciar a thread e passar o handle do formulário
  TMyThread2.Create(Self.Handle);
end;
  

Explicação do Código

  1. Definição da Constante de Mensagem:
    • WM_UPDATE = WM_USER + 1;: Define uma constante para a mensagem personalizada que será enviada da thread para o formulário.
  2. Definição da Classe da Thread:
    • TMyThread: Classe que herda de TThread e sobrescreve o método Execute para realizar uma operação em segundo plano.
    • constructor Create(AHandle: HWND): Construtor que aceita um HWND (handle da janela) como parâmetro e inicia a thread.
    • procedure Execute: Método onde a operação de longa duração é realizada. Após a operação, a thread envia uma mensagem para a interface gráfica usando PostMessage.
  3. Sobrescrever o Método WndProc:
    • WndProc(var Msg: TMessage): Método sobrescrito para capturar a mensagem personalizada WM_UPDATE. Quando a mensagem é recebida, uma mensagem é mostrada ao usuário.
  4. Iniciar a Thread a partir do Formulário:
    • Button1Click: Método que é chamado quando o botão é clicado. Ele cria uma nova instância de TMyThread, passando o handle do formulário para que a thread possa enviar mensagens de volta para ele.

Com este exemplo, você pode ver como criar uma thread personalizada em Delphi e utilizar PostMessage para comunicar a partir da thread com a interface gráfica de forma segura e eficiente.

Veja abaixo a ilustração do projeto utilizando mensagens:

Ilustração Mensagens.

3. Seções Críticas

Quando várias threads precisam acessar um recurso compartilhado, é crucial utilizar uma seção crítica para evitar conflitos.

Nesse exemplo iremos criar 5 Threads que irão compartilhar a variável SharedResource, que será incrementada cada vez que uma nova Thread é criada, e sincronizará os dados na interface gráfica. Dessa forma a variável SharedResource será manipulada de forma segura, através da seção crítica, sempre que uma Thread for utilizá-la.

Passo 1: Criar, inicializar e finalizar uma TCriticalSection. Criar a variável SharedResource que será o recurso compartilhado.

Delphi

var
  CriticalSection: TCriticalSection;
  SharedResource: Integer;
  
procedure TForm1.FormCreate(Sender: TObject);
begin
  CriticalSection := TCriticalSection.Create; //Cria a instância de sessão crítica
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Assigned(CriticalSection) then
    CriticalSection.Free;//Finaliza a instância de sessão crítica
end;
  

Passo 2: Implementar a classe da thread que faz uso da seção crítica para acessar o recurso compartilhado.

Delphi

type  
  TMyThread3 = class(TThread)
  private
    FUpdateMethod: TThreadProcedure;
  protected
    procedure Execute; override;
  public
    constructor Create(UpdateMethod: TThreadProcedure);
  end;
  
constructor TMyThread3.Create(UpdateMethod: TThreadProcedure);
begin
  inherited Create(True); // Create suspended
  FUpdateMethod := UpdateMethod;
  FreeOnTerminate := True;
end;

procedure TMyThread3.Execute;
begin
  CriticalSection.Enter;
  try
    SharedResource := SharedResource + 1; // Acesso seguro
    Sleep(1000); //Espera 1 segundo para dar tempo de exibir o incremento na interface gráfica
    Synchronize(FUpdateMethod); // Atualizar a interface de forma segura
  finally
    CriticalSection.Leave;
  end;
end;
  

Passo 3: Criar instâncias da thread e iniciar sua execução.

Delphi

procedure TForm1.Button3Click(Sender: TObject);
var
  i: Integer;
begin
  SharedResource := 0;
  for i := 1 to 5 do
  begin
    TMyThread3.Create(UpdateUI).Start;
  end;
end;
  

Passo 4: Mostrar como utilizar o resultado da thread na interface gráfica ou outro lugar no código principal.

Delphi

procedure TForm1.UpdateUI;
begin
  //Atualiza a interface gráfica com os dados compartilhados
  Label1.Caption := 'SharedResource value: ' + SharedResource.ToString;
end;
  

Explicação do Código

Unit1.pas:

  • Declaração das variáveis e tipos principais:
    • CriticalSection: TCriticalSection: Seção crítica para controlar o acesso ao recurso compartilhado.
    • SharedResource: Integer: Recurso compartilhado entre as threads.
  • TMyThread3:
    • Construtor Create(UpdateMethod: TThreadProcedure): Recebe um método para atualizar a interface quando a thread termina sua execução.
    • Método Execute: Faz o processamento na thread. A seção crítica é usada para garantir acesso exclusivo ao SharedResource. O método Synchronize é usado para atualizar a interface de forma segura.
  • TForm1:
    • Botão Button3: Ao ser clicado, cria e inicia múltiplas threads.
    • Método UpdateUI: Atualiza a interface gráfica com o valor do SharedResource.

Veja abaixo a ilustração do projeto utilizando Seções críticas para Recursos compartilhados:

Ilustração Recursos Compartilhados com Sincronização.

4. Filas (Queues)

As filas podem ser utilizadas para gerenciar a troca de dados entre threads, garantindo que as tarefas sejam processadas na ordem em que foram adicionadas.

Nesse exemplo vamos criar a classe TTaskQueue que será responsável por criar as tarefas, a Thread TProducerThread que será responsável por colocar as tarefas na fila e a Thread TConsumerThread que será responsável por consumir as tarefas da fila. Para auxiliar nesse projeto iremos utilzar um button e um memo para auxiliar na exibição da execução da fila.

Passo 1: Declarar a variável que será responsável por armazenar a fila

Delphi

private
  TaskQueue: TTaskQueue;
  

Passo 2: Criar e implementar a Classe TTaskQueue

Delphi

type
   FQueue: TQueue<Integer>;
    FLock: TCriticalSection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddTask(Value: Integer);
    function GetNextTask: Integer;
  end;
  
constructor TTaskQueue.Create;
begin
  FQueue := TQueue<Integer>.Create;
  FLock := TCriticalSection.Create;
end;

destructor TTaskQueue.Destroy;
begin
  FQueue.Free;
  FLock.Free;
  inherited;
end;

procedure TTaskQueue.AddTask(Value: Integer);
begin
  FLock.Enter;
  try
    FQueue.Enqueue(Value);
  finally
    FLock.Leave;
  end;
end;

function TTaskQueue.GetNextTask: Integer;
begin
  FLock.Enter;
  try
    if FQueue.Count > 0 then
      Result := FQueue.Dequeue
    else
      Result := -1; // ou algum valor padrão
  finally
    FLock.Leave;
  end;
end;
  

Passo 3: Criar e implementar a Thread TProducerThread

Delphi

type

  TProducerThread = class(TThread)
  private
    FTaskQueue: TTaskQueue;
    FTaskCount: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(TaskQueue: TTaskQueue; TaskCount: Integer);
  end;
  
constructor TProducerThread.Create(TaskQueue: TTaskQueue; TaskCount: Integer);
begin
  inherited Create(True);
  FTaskQueue := TaskQueue;
  FTaskCount := TaskCount;
  FreeOnTerminate := True;
end;

procedure TProducerThread.Execute;
var
  I: Integer;
begin

  for I := 1 to FTaskCount do
  begin
    FTaskQueue.AddTask(I);
    Sleep(100); // Simula algum trabalho
  end;

end;
  

Passo 4: Criar e implementar a Thread TConsumerThread

Delphi

type
  TConsumerThread = class(TThread)
  private
    FTaskQueue: TTaskQueue;
    FUpdateMethod: TThreadProcedure;
  protected
    procedure Execute; override;
  public
    constructor Create(TaskQueue: TTaskQueue; UpdateMethod: TThreadProcedure);
  end;
  
constructor TConsumerThread.Create(TaskQueue: TTaskQueue; UpdateMethod: TThreadProcedure);
begin
  inherited Create(True);
  FTaskQueue := TaskQueue;
  FUpdateMethod := UpdateMethod;
  FreeOnTerminate := True;
end;

procedure TConsumerThread.Execute;
var
  Task: Integer;

begin

  while not Terminated do
  begin
    Task := FTaskQueue.GetNextTask;
    if Task <> -1 then
    begin
      // Processar a tarefa
      Sleep(150); // Simula algum trabalho

      // Atualizar a interface
      Synchronize(FUpdateMethod);
    end
    else
    begin
      // Sem tarefas, dormir um pouco
      Sleep(50);
    end;
  end;

end;
  

Passo 5: Instanciar a Classe TTaskQueue e as Threads TProducerThread e TConsumerThread e iniciar a execução.

Delphi

procedure TForm1.Button4Click(Sender: TObject);
var
  ProducerThread: TProducerThread;
  ConsumerThread: TConsumerThread;

begin
  Memo1.Lines.Clear;
  TaskQueue := TTaskQueue.Create;

  // Criar e iniciar threads produtoras e consumidoras
  ProducerThread := TProducerThread.Create(TaskQueue, 10);
  ConsumerThread := TConsumerThread.Create(TaskQueue, UpdateLog);

  ProducerThread.Start;
  ConsumerThread.Start;
end;
  

Passo 6: Mostrar como exibir a fila sendo consumida.

Delphi

procedure TForm1.UpdateLog;
begin
  Memo1.Lines.Add('Task processed by consumer.');
end;
  

Explicação do Código

  1. Classe TTaskQueue:
    • Construtor e Destrutor: Inicializa e limpa os recursos, garantindo que o TQueue e a TCriticalSection sejam criados e destruídos corretamente.
    • Métodos AddTask e GetNextTask: Manipulam a fila de tarefas de forma segura, usando a TCriticalSection para sincronização.
  2. Classe TProducerThread:
    • Construtor: Inicializa a thread com a fila de tarefas e o número de tarefas a serem produzidas.
    • Método Execute: Adiciona tarefas à fila e simula algum trabalho com Sleep.
  3. Classe TConsumerThread:
    • Construtor: Inicializa a thread com a fila de tarefas e o método de atualização da interface.
    • Método Execute: Obtém e processa tarefas da fila, sincronizando a atualização da interface com Synchronize.
  4. Formulário TForm1:
    • Método StartButtonClick: Inicializa a fila de tarefas e cria threads produtoras e consumidoras.
    • Método UpdateLog: Atualiza a interface gráfica (um memo) para indicar que uma tarefa foi processada.

Como Funciona

  • Thread Produtora: Adiciona tarefas à fila.
  • Thread Consumidora: Retira tarefas da fila e processa-as, atualizando a interface do usuário para refletir o progresso.
  • Sincronização: A TCriticalSection garante que apenas uma thread possa acessar a fila de tarefas de cada vez, prevenindo condições de corrida e garantindo a integridade dos dados.

Veja abaixo as ilustrações do projeto utilizando Filas(Queues):

Ilustração de Queue(Fila).

Código fonte do exemplo

Você pode fazer o download do exemplo do projeto através do repositório do github:

https://github.com/Gisele-de-Melo/ComunicacaoEntreThreads

Se você gostou desse conteúdo, clica no botão abaixo e compartilha com os amigos para me ajudar a espalhar conhecimento.

Conclusão

A comunicação entre threads é um aspecto crítico na programação multithreaded, e é essencial aplicar as técnicas corretas para evitar problemas de sincronização. A escolha do método dependerá do caso de uso específico, e é importante considerar o desempenho e a segurança dos dados.

Neste artigo, exploramos algumas das abordagens mais comuns em Delphi para garantir uma comunicação segura e eficaz entre threads. Ao implementar essas técnicas, você estará melhor preparado para criar aplicações robustas e responsivas.

Essas práticas não apenas melhoram a estrutura do seu código, mas também aumentam a manutenibilidade e a escalabilidade das suas aplicações. Assim, a comunicação entre threads torna-se uma habilidade valiosa para qualquer desenvolvedor Delphi.

Posts Relacionados



Nenhum comentário:

Postar um comentário

Recursividade em Delphi: Conceitos, Exemplos e Aplicações Práticas

Neste post, você aprenderá quando usar recursividade em Delphi, suas vantagens e desvantagens, além de um exemplo prático implementa...