[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

15 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é.

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.