Hoje vou apresentar os conceitos básicos da programação paralela na linguagem C#. |
Tanto a programação assíncrona quanto a paralela não são novas na plataforma .NET.
O modelo da
programação assíncrona ou Asynchronous Programming
Model (APM) é o modelo mais antigo e está disponível desde a versão
1.0.
Já o modelo de programação em paralelo ou Task
Parallel Library (TPL) foi introduzido na versão 4.0 da plataforma
.NET
Como uma forma de tentar melhorar o primeiro modelo. Assim a TPL é uma
grande melhoria em relação aos modelos anteriores, ela
simplifica o processamento paralelo e faz melhor uso dos recursos do sistema
e com a TPL podemos implementar a Programação Paralela em C# .NET de maneira
muito fácil.
Na versão 5.0 da linguagem C# foram introduzidas as palavras async e await de forma a facilitar a programação assíncrona, e, usando async e await você pode escrever o código assíncrono da mesma maneira que escreve o seu código síncrono e o compilador cuida de toda a complexidade e libera você para fazer escrever o seu código.
A primeira coisa a fazer é destacar a diferença entre Programação Paralela e Programação Assíncrona, elas possuem conceitos relacionados mas não são a mesma coisa.
Programação paralela
A programação paralela é usada para dividir uma tarefa em diversas partes e executar estas partes de forma simultânea.
Como exemplo você pode cantar e tomar banho ao mesmo tempo e assim esta realizando estas tarefas em paralelo.
Além disso podemos ter dois tipos de paralelismo :
Programação assíncrona
A programação assíncrona permite gerenciar as threads dos processos de forma eficiente evitando que uma thread seja bloqueada enquanto aguarda o processamento de outra thread
Aqui temos dois
conceitos importantes : threads e processos.
Os processos significam qualquer programa em execução e as threads são
linhas ou sequências de execução de código que podem ser executadas
independentemente umas das outras. Dessa forma uma thread é a menor
unidade de tarefa que pode ser executada por um sistema operacional e é um
segmento de um processo enquanto que o processo pode ter múltiplas threads.
A programação
assíncrona ocorre quando uma tarefa é executada, e você pode alternar para uma
tarefa diferente sem esperar que a tarefa atual seja concluída e você faz isso
sem interromper ou bloquear a tarefa.
Imagine que você tivesse que fazer um lanche e lavar suas roupas na máquina de
lavar. Você poderia colocar suas roupas na máquina de lavar e, sem esperar
essa tarefa ser concluída, você poderia ir fazer o lanche. Aqui, você
executou essas duas tarefas de forma assíncrona.
Dessa forma a programação assíncrona nos ajuda a alcançar a concorrência.
Task Parallel Library ou TPL
A Task Parallel Library (TPL) é um conjunto de tipos públicos e APIs presentes nos namespaces System.Threading e System.Threading.Tasks.
Para realizar tarefas simples usando programação paralela basta usar o método Invoke da classe Parallel do namespace System.Threading passando uma instância do delegate Action para cada método que desejamos executar.
Sintaxe : Parallel.Invoke( Action[])
Assim basta fornecer um conjunto de delegates Action, cada um dos quais envolve um método que você deseja executar.
No exemplo a seguir usamos o método Invoke para para processar os métodos ExibirDias e ExibirMeses em paralelo :
System.Threading ... Parallel.Invoke( new Action(exibirDias), new Action(exibirMeses) ); ... |
Para realiza iterações em paralelo temos dois métodos disponíveis : Parallel.For e Parallel.Foreach.
Usando o Parallel.For
Este método executa um loop for no qual as iterações podem ser executadas em paralelo.
Exemplo:
System.Threading.Tasks
... ... |
Nesta sobrecarga do método Parallel.For estamos usando um valor inteiro inicial inclusivo (0) , um valor inteiro final exclusivo (10) e uma Action que representa o código a ser executado em paralelo.
Este código é equivalente a um laço for: for (int i = 0; i < 10; i++) {}
No código abaixo temos um exemplo que compara a execução deste dois tipos de laço for:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ParallelFor
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Pressione ENTER para iniciar");
Console.ReadLine();
ProcessarLaco();
Console.WriteLine("\n\n");
Parallel.For(0, 11, i => Console.WriteLine(i + "\t"));
Console.ReadLine();
}
private static void ProcessarLaco()
{
for (int i = 0; i < 11; i++)
{
Console.WriteLine(i + " Thread : = " +
Thread.CurrentThread.ManagedThreadId + "\t");
Thread.Sleep(500);
}
}
}
}
|
Usando o Parallel.Foreach
O método
Parallel.Foreach executa uma operação foreach
no qual as iterações podem ser executadas em paralelo.
Assim vamos supor que temos uma lista de strings com nomes de frutas :
List<string>
frutas = new List<string>(); frutas.Add("Maça"); frutas.Add("Banana"); frutas.Add("Abacaxi"); frutas.Add("Melancia"); frutas.Add("Pêra"); frutas.Add("Uva"); frutas.Add("Figo"); |
Para percorrer de forma síncrona esta lista podemos usar um laço foreach na abordagem tradicional da linguagem C# :
foreach (string fruta in frutas) { WriteLine($"Fruta : {fruta}, Thread Id= {Thread.CurrentThread.ManagedThreadId} \t"); } |
Aqui, neste iterador foreach, apenas uma thread será usada para cada um dos itens a ser processado, então enquanto o primeiro item está sendo processado, os outros estão aguardando.
Se tivermos um computador com um processador com 5 threads disponíveis podemos acelerar o processando usando o método Parallel.Foreach :
Parallel.ForEach(frutas, fruta => { WriteLine($"Fruta : {fruta}, Thread Id= {Thread.CurrentThread.ManagedThreadId} \t"); } ); |
Neste código o
foreach do Parallel
descobre e utiliza todos as threads disponíveis do seu processador para
que a ação seja executada paralelamente, então cada thread vai processar
um item da lista ao mesmo tempo.
Considerando que levaria 1 minuto para cada processamento, o
foreach padrão levaria um total de 4 minutos,
enquanto que usando Parallel.ForEach levaria 1
minuto considerando 4 itens e um processador com 4 threads.
Veja o código completo:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;
namespace ParallelForeach
{
internal class Program
{
private static void Main(string[] args)
{
List<string> frutas = new List<string>();
frutas.Add("Maça");
frutas.Add("Banana");
frutas.Add("Abacaxi");
frutas.Add("Melancia");
frutas.Add("Pêra");
frutas.Add("Uva");
frutas.Add("Figo");
frutas.Add("Pêssego");
frutas.Add("Laranja");
frutas.Add("Kiwi");
frutas.Add("Manga");
frutas.Add("Melão");
frutas.Add("Morango");
Console.WriteLine("Pressione ENTER para iniciar");
Console.ReadLine();
WriteLine("Exibindo a lista usando laço foreach \n");
foreach (string fruta in frutas)
{
WriteLine($"Fruta : {fruta}, Thread Id= {Thread.CurrentThread.ManagedThreadId} \t");
}
WriteLine("\n\nExibindo a lista usando Parallel.ForEach");
Parallel.ForEach(frutas, fruta =>
{
WriteLine($"Fruta : {fruta}, Thread Id= {Thread.CurrentThread.ManagedThreadId} \t");
}
);
Read();
}
}
}
|
Agora, tanto o
Parallel.For como o Parallel.ForEach possuem várias
sobrecargas que permitem parar ou interromper a execução do loop,
monitorar o estado do circuito em outros segmentos, manter o estado de segmento
local, finalizar objetos na thread-local, controlar o grau de
concorrência, e assim por diante.
Dessa forma a programação paralela fornece um grande poder de processamento mas devemos ter cautela ao usar esse recurso.
Quando usar e quando não usar a programação paralela
Geralmente devemos usar este recurso quando tivermos muitas operações que dependam de processamento de CPU (CPU-Bound) e que possam ser executadas de maneira paralela quando houver mais de uma ocorrência de item a ser processado.
Mas utilizar o Parallel.ForEach ou Parallel.For em operações/iterações simples que não utilizam muitos recursos não garantia de ser mais rápido do que um foreach padrão.
Na verdade o custo
de alocar threads da maneira que a classe Parallel
faz pode causar uma "overhead" que pode deixar seu código mais lento.
Então o "peso" da operação a ser executada dentro do
foreach é o que vai ditar se compensa ou não a
utilização da forma paralela.
Outro detalhe importante é que a programação paralela não é indicada para o
ambiente web.
Como no ambiente web temos um ambiente de alta demanda onde cada thread
tratar um request não seria recomendável ocupar várias threads
para processar uma única requisição.
No próximo artigo veremos como executar Task usando TaskFactory.StartNew e usar os métodos Wait, WaitAll e WatiAny.
"Bendito o Deus e Pai de nosso Senhor
Jesus Cristo, o qual nos abençoou com todas as bênçãos espirituais nos lugares
celestiais em Cristo; Como também nos elegeu nele antes da fundação do
mundo, para que fôssemos santos e irrepreensíveis diante dele em amor;"
Efésios 1:3,4
Referências:
C# 8.0 - A instrução switch - Macoratti
C# - Programação Funcional - Exemplos
C# 8.0 - As novidades da nova versão
C# - Coleções Imutáveis - Macoratti
C# - O que há de novo com o C# 9.0
C# 9.0 - Apresentando Records
C# - Sintaxe e conceitos básicos
C# - Os 10 Erros mais comuns dos iniciantes
C# - Otimizando o código
.NET - Apresentando Parallel LINQ (PLINQ)
C# - Programação Paralela
C# - Concorrência , Paralelismo e Assincronismo no .NET
Programação paralela com PLINQ