[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.

Introdução

Em março deste ano, apliquei um treinamento sobre Delphi Seattle na 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.

Cenário

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:

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

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

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

Tempo final das consultas sem utilizar Multithreading

Melhorando a funcionalidade com processamento paralelo

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:

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:

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!

Agora, o TParallel.For

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:

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 utilizaremos 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:

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!


 

André Celestino