[Delphi] Utilizando o mecanismo de processamento paralelo

[Delphi] Utilizando o mecanismo de processamento paralelo

Fala, galera!
Acredito que muitos de vocês já tenham usado, estudado ou ao menos ouvido falar de Multithreading do Delphi. As bibliotecas deste recurso estão presentes desde a versão XE7, porém, por serem relativamente recentes, às vezes esquecemos de sua existência. O objetivo deste artigo é apresentar um cenário no qual o uso de Multithreading pode trazer uma grande vantagem em relação ao tempo de resposta de uma aplicação.

 

Em março deste ano, apliquei um treinamento sobre Delphi Seattle na DB1 Global Software, empresa em que eu trabalho, para apresentar os recursos das versões mais recentes do Delphi, entre eles, claro, a biblioteca de Multithreading. Com esse mecanismo, podemos distribuir processamentos complexos em threads distintas, executando-as ao mesmo tempo. Por estarem em fluxos paralelos, o tempo de término destes processamentos é evidentemente menor do que uma abordagem sequencial (ou “linear”).
Muitos me questionaram sobre a aplicação de um processamento paralelo em um cenário mais próximo da realidade, portanto, dediquei os últimos dias para elaborar este artigo.

Considere a emissão de um relatório de pedido que possui as seguintes seções:

  • Dados do cliente (nome, endereço, CPF…);
  • Dados do pedido (data, total, forma de pagamento…);
  • Dados dos itens do pedido (produto, descrição, quantidade…).

Para que a emissão seja realizada, é necessário consultar os dados dessas três seções de forma separada. Como exemplo, considere que cada consulta demore aproximadamente os tempos abaixo:

  • Dados do cliente: 2 segundos
  • Dados do pedido: 3 segundos
  • Dados dos itens do pedido: 4 segundos

 

Habitualmente, a codificação para a emissão deste relatório poderia ser:

ConsultarDadosCliente;
ConsultarDadosPedido;
ConsultarDadosItensPedido;

Para simular o tempo dispendido com essa operação, programei um Sleep() dentro de cada método com a respectiva duração:

procedure ConsultarDadosCliente;
begin
  Sleep(2000);
end;
 
procedure ConsultarDadosPedido;
begin
  Sleep(3000);
end;
 
procedure ConsultarDadosItensPedido;
begin
  Sleep(4000);
end;

Em seguida, adicionei também duas variáveis para calcular o tempo gasto e uma mensagem para exibi-lo ao término das consultas:

procedure EmitirRelatorio;
var
  Inicio: TDateTime;
  Fim: TDateTime;
begin
  Inicio := Now;
 
  ConsultarDadosCliente;
  ConsultarDadosItensPedido;
  ConsultarDadosPedido;
 
  Fim := Now;
 
  ShowMessage(Format('Consultas realizadas em %s segundos.',
    [FormatDateTime('ss', Fim - Inicio)]));
end;

Ao executar o método de emissão, receberemos a mensagem:

Tempo final das consultas sem utilizar Multithreading

 

O propósito deste artigo é mostrar que podemos melhorar isso. Com o recurso de processamento paralelo, podemos distribuir cada consulta em uma thread e executá-las simultaneamente. Para isso, trabalharemos com a classe TTask, da unit System.Threading, utilizando a seguinte sintaxe:

uses
  System.Threading;
 
{ ... }
 
var
  Task: ITask;
begin
  Task := TTask.Create({método});
  Task.Start;
end;

 

Basta então criar uma Task para cada consulta e disparar todas elas?
Sim, mas há uma condição que exige a nossa atenção. Você deve ter notado que cada consulta tem uma duração diferente. Isso significa que, se executarmos todas elas em threads, é possível que as consultas mais demoradas não sejam finalizadas a tempo. Caso isso ocorra, os dados dessas seções não serão exibidos no relatório.
Para evitar esse comportamento, criaremos um array de ITask e, ao final, utilizaremos um método chamado WaitForAll para solicitar que o processamento principal só prossiga quando todas as threads forem concluídas. Confira a codificação:

procedure EmitirRelatorio;
var
  Tasks: array [0..2] of ITask;
 
  Inicio: TDateTime;
  Fim: TDateTime;
begin
  Inicio := Now;
 
  Tasks[0] := TTask.Create(ConsultarDadosCliente);
  Tasks[0].Start;
 
  Tasks[1] := TTask.Create(ConsultarDadosPedido);
  Tasks[1].Start;
 
  Tasks[2] := TTask.Create(ConsultarDadosItensPedido);
  Tasks[2].Start;    
 
  TTask.WaitForAll(Tasks);
 
  Fim := Now;
 
  ShowMessage(Format('Consultas realizadas em %s segundos.',
    [FormatDateTime('ss', Fim - Inicio)]));
end;

Agora, ao executar o mesmo método de emissão, este é o resultado de tempo total:

Tempo final das consultas utilizando Multithreading

Concluindo, com o processamento em paralelo, reduzimos a emissão do relatório para metade do tempo!

 

André, você poderia citar outros exemplos?
Sim!
Um fechamento de caixa é outro cenário bem comum no qual o Multithreading pode ser aplicado, já que consiste em uma série de operações, como conferência de valores, cálculo de entradas e saídas, geração de saldos e até a produção de gráficos. Mesmo assim, para dar continuidade sobre este recurso, vou apresentar mais um cenário. Imagine uma rotina de processamento de arquivos em lote através da seguinte codificação:

var
  ListaArquivos: TStringList;
  i: integer;
begin
  ListaArquivos := TStringList.Create;
 
  ListaArquivos.Text := CarregarArquivos;
 
  for i := 0 to Pred(ListaArquivos.Count) do
  begin
    ProcessarArquivo(ListaArquivos[i]);
  end;
 
  ListaArquivos.Free;
end;

 

Observamos, claro, que um arquivo será processado por vez, já que o loop é iterativo e sequencial. Esse mesmo procedimento também pode ser distribuído em threads paralelas, no entanto, para este caso, não utilizamos o TTask. Devemos utilizar um comando chamado TParallel.For, contida na mesma biblioteca. Além disso, como haverá um acesso concorrente ao objeto ListaArquivos, é necessário utilizar um mecanismo de semáforo, representado pelo método TThread.Queue no código abaixo:

var
  ListaArquivos: TStringList;
begin
  ListaArquivos := TStringList.Create;
 
  ListaArquivos.Text := CarregarArquivos;
 
  TParallel.For(0, Pred(ListaArquivos.Count),
            procedure (i: integer)
            begin
              TThread.Queue(TThread.CurrentThread,
                procedure
                begin
                  ProcessarArquivo(ListaArquivos[i]);
                end)
            end);
end;

Explicando: ao invés de usar um único fluxo de processamento, o TParallel.For distribuirá as iterações em threads, ou seja, o loop será “dividido” em fluxos paralelos, reduzindo (bastante!) o tempo.

 

Fico por aqui, pessoal! Espero que esse artigo incentive a utilização dessa poderosa biblioteca do Delphi.
Um grande abraço!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

29 comentários

  1. Conteúdo muito bom e bem organizado, gostei… Só não ficou muito claro pra mim, o código:

    TThread.Queue(TThread.CurrentThread,
                    procedure
                    begin
                      ProcessarArquivo(ListaArquivos[i]);
                    end)

    No caso, qual é a finalidade do TThread.Queue nessa situação?

    Abraços, professor!

    1. Olá, Danilo!
      Essa instrução é necessária para que o mesmo objeto (neste caso, “ListaArquivos”) não seja acessada ao mesmo tempo por duas ou mais threads. Elas devem aguardar a thread atual terminar o processamento com o objeto para poder usá-lo. É o conceito de semáforo que normalmente é abordado na disciplina de Sistemas Operacionais nos cursos superiores.

      Abraço!

  2. Bom dia André,
    Cara que top isso hein, show.
    Já estou pensando em criar uma função para chamar esse método em outros processo do sistema, passando como parâmetro o método a ser executado.
    Abraço André.

  3. Muito bom seu artigo!!! Mais uma técnica para aplicar!!!!

    Grande abraço, e obrigado por compartilhar…

  4. Bom dia André. Estou desenvolvendo um algoritmo genético em Delphi, usando generics, anonimous methods e multithreading. Seus artigos tem sido de grande ajuda! Como você faria para PAUSAR uma iTask, e depois retomar o processo de onde parou?

    1. Olá, Ricardo, tudo bem? Desculpe-me pela demora.
      Excelente pergunta. Ainda não tive a oportunidade de trabalhar em um cenário no qual é necessário pausar uma ITask, mas vou realizar alguns testes e entrar em contato com você por e-mail, ok?

      Abração!

  5. Bom dia André, ótimo o artigo. Me tornei leitor assíduo aqui. Eu quero usar o recurso para geração de um arquivo que vem de um ecf. Em modo de debug tá perfeito, porém quando eu passo para release, no momento de executar o procedimento na thread, o o Windows congela. Só posso recorrer ao CTRL ALT DEL ou aguardar.
    Você sabe por que funciona em debug e em release não? Desde já, muito obrigado.

  6. Muito bom parabéns, não sei se estou falando besteira mais nesse caso daria para usar um progressbar usando o TTask sem travar o formulário ?

    1. Olá, Erasmo!
      Exatamente! É possível utilizar TTask para atualizar um controle visual, como um componente TProgressBar:

      var
        aTask: ITask;
      begin
        atask := TTask.Create(procedure
        begin
          while ProgressBar1.Position < 100 do
          begin
            Sleep(1000);
            ProgressBar1.StepIt;
          end;
        end);
        aTask.Start;
      end;

      Legal, hein?
      Abraço!

  7. Prezado André.

    Eu estou precisando utilizar o recurso de processamento paralelo, porém a versão que estou trabalhando é o Delphi 2007 e como você comentou que as bibliotecas deste recurso estão presentes desde a versão XE7, entendo que não conseguirei utilizar este recurso, correto? Tem alguma alternativa que você sugeriria aplicar este recurso na versão do Delphi 2007?

    Att.,

    Júlio Bitencourt.

    1. Olá, Julio, boa noite!
      No Delphi 2007, você pode utilizar a biblioteca OmniThreadLibrary, disponível no link abaixo:

      http://otl.17slon.com/

      Não tive a oportunidade de testá-la, mas, pelos comentários que já acompanhei na internet, parece ser bem produtiva.

      Abraço!

  8. Parabéns pelo artigo, só queria tirar uma dúvida, caso tenha rodando um processo pelo TTask e o cliente clique para fechar o sistema, existe algum teste para saber se tem algum Task rodando para avisar e evitar que a aplicação seja finalizada?

  9. Olá André! Muito esclarecedor o seu artigo sobre TTask.
    Cheguei aqui porque está justamente pesquisando sobre ele, pois tenho um módulo no meu programa que preciso saber se um FTP está ativo ou não. Eu faço isso quando o programa é aberto e, caso o FTP esteja off-line, o programa leva cerca de 5 segundos para “descobrir” isso. Pensei em colocar como uma tarefa, ou seja, deixo o usuário começar a trabalhar enquanto faço a verificação.
    Eu já tenho uma procedure na minha unit que faz isso. Bastaria eu usar:

    var
      Task: ITask;
    begin
      Task := TTask.Create(Verifica_FTP);
      Task.Start;
    end;

    Seria só isso mesmo? Eu colocaria isso no on-create do meu form principal?

    Agradeço antecipado a ajuda e parabenizo mais uma vez pelo blog!

    Um abraço!

    1. Olá, Adilson, boa noite!
      Em primeiro lugar, parabenizo você pela ótima escrita! Ficou muito fácil entender a sua dúvida.
      Bom, Adilson, a sua necessidade é justamente o que o ITask se propõe: executar uma tarefa em paralelo para não prejudicar a experiência do usuário. Acredito que o seu código irá funcionar conforme esperado.
      Opcionalmente, você pode escrever tudo em uma linha só:

      TTask.Create(Verifica_FTP).Start;

      Grande abraço e muito obrigado!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.