C# - Programação Paralela : Criando e gerenciando threads
Hoje vamos recordar conceitos da programação paralela com C# e o uso da classe Thread e como gerenciar a criação de threads com ThreadPool e BackgroundWorker. |
Imagine que temos um aplicativo com tarefas independentes. Que tal aumentar o desempenho do aplicativo usando a classe Thread ?
Essa abordagem
também funciona bem para grandes tarefas. Isso nos permite dividir o
processamento em tarefas menores e executá-los de forma simultânea.
A maneira mais simples e fácil de criar uma thread é usando a classe
Thread do namespace System.Threading.
Basicamente, precisamos fornecer o nome do método para nossa instância
Thread por meio do delegate
ThreadStart.
Vamos mostrar a seguir um exemplo de código em um projeto Console (.NET Core) no ambiente do NET 6.0 :
Console.WriteLine("### -
Usando Thread ### \n");
Console.WriteLine("Iniciar"); Console.ReadLine(); static
void CriarThreadSemParametro()
var thread = new Thread(new ThreadStart(CriarValoresAleatorios)); static
void CriarValoresAleatorios() |
Resultado:
Além disso, podemos fornecer um parâmetro para o método com o delegate
ParameterizedThreadStart que representa o método
que será executado em uma Thread.
Abaixo vemos um exemplo de código :
Console.WriteLine("### - Usando
Thread com parâmetros ### \n"); Console.WriteLine("Inicio"); CriaThreadComParametro();
Console.WriteLine("Termino");
static void CriaThreadComParametro()
static void CriaValoresAleatorios(object
limite) |
Quando uma thread gerenciada é criada, o método executado na thread é representado por:
Assim, thread não começa a ser executada até que o Thread.Start método seja chamado, e, o delegate ThreadStart. ParameterizedThreadStart ou é invocado na thread, e a execução começa na primeira linha do método representado pelo delegate. No caso do delegate ParameterizedThreadStart , o objeto que é passado para o método Start(Object) é passado para o delegate.
Dessa forma, o parâmetro do método deve ser um tipo de objeto, porque precisamos respeitar a assinatura do delegate.
Além disso o delegate ParameterizedThreadStart oferece suporte a apenas um único parâmetro. Podemos passar vários itens de dados para o ParameterizedThreadStart fazendo com que esse parâmetro seja um dos seguintes:
Infelizmente, existem algumas preocupações a serem consideradas quando estamos implementando threads:
Gerenciando a criação de Threads
Sabemos que um
grande número de threads pode diminuir o desempenho do nosso aplicativo. Então,
como podemos gerenciar a criação de threads ?
O Common Language Runtime (CLR) tem um algoritmo
para determinar o número ideal de threads com base na carga da CPU, conhecido
como ThreadPool.
Assim podemos usar ThreadPool para gerenciar a
criação e execução de threads em nosso aplicativo. O código a seguir apresenta
como podemos usar a classe ThreadPool do namespace System.Threading.
Console.WriteLine("### - Criando Threads usando ThreadPool ### \n");
Console.WriteLine("Início");
CriarThreadUsandoThreadPool();
Console.WriteLine("Término");
Console.ReadLine();
static void CriarThreadUsandoThreadPool()
{
Console.WriteLine("------\n");
int limite = 20;
ThreadPool.QueueUserWorkItem(new WaitCallback(CriaValoresSequenciais), limite);
}
static void CriaValoresSequenciais(object limite)
{
var end = int.Parse(limite.ToString());
for (int i = 1; i <= end; i++)
{
Console.Write($"{i.ToString("000")}\t");
}
}
|
Resultado :
O método
QueueUserWorkItem enfileira um método para execução
em uma thread do pool de threads. Em nosso caso, estamos enfileirando o método
CriaValoresSequenciais.
Observe que usamos um delegado WaitCallback, que
representa um ponteiro para o método que será executado em uma thread do pool de
threads. Ele será executado quando houver uma thread disponível para isso.
Podemos ainda usar a classe BackgroundWorker que executa uma operação em outra thread e permite gerenciar as threads de um ThreadPool facilmente. Esta classe fornece alguns eventos úteis para lidar com a execução de nossas threads, e, podemos gerenciar as threads com eventos ativando as propriedades:
Depois de habilitar essas propriedades, precisamos nos inscrever em três eventos:
Além disso esta classe apresenta também as propriedades:
O código a seguir apresenta um exemplo de como implementar esses eventos:
using System.ComponentModel; Console.WriteLine("### - Usando BackgroundWorker ### \n"); Console.WriteLine("Início");
var backgroundWorker = new BackgroundWorker
backgroundWorker.DoWork
+= BackgroundWorker_DoWork;
int valorLimite = 50;
Thread.Sleep(5000);
Console.WriteLine("Término");
static void BackgroundWorker_RunWorkerCompleted(object
sender, RunWorkerCompletedEventArgs e)
static void BackgroundWorker_ProgressChanged(object
sender, ProgressChangedEventArgs e)
static void BackgroundWorker_DoWork(object
sender, DoWorkEventArgs e) int soma = 0;
for (int i = 1; i <= limite; i++) soma += i;
var percentual = ((double)i / limite) * 100;
Thread.Sleep(200);
e.Result = soma; |
Resultado:
Cabe destacar neste código o método CancelAsync.
Quando habilitamos
a propriedade WorkerSupportsCancellation, podemos
chamar esse método para enviar um sinal de cancelamento para nossa thread.
Quando enviamos esse sinal, a propriedade
CancelamentoPending torna-se verdadeira, então podemos fazer o método
CancelAsync da thread atual sair normalmente.
A desvantagem em usar o BackgroundWorker é quanto mais threads
temos, fica mais difícil de
depurar e manter o código.
Pegue o código do exemplo: UsandoThread
"O homem bom, do
bom tesouro do seu coração tira o bem, e o homem mau, do mau tesouro do seu
coração tira o mal, porque da abundância do seu coração fala a boca."
Lucas 6:45
Referências:
C# - Lendo e escrevendo em arquivos textos e binários
C# - Entendo o I/O na plataforma .NET
C# - Fluxo assíncrono ou async streams
C#- Apresentando Streams assíncronos