Programação Assíncrona (Multithreading) em .NET com C#
Veja neste artigo o que é, quando e como utilizar multithread. Thread é uma forma de um processo dividir a si mesmo em duas ou mais tarefas, podendo executar elas concorrentemente. O suporte a threads é oferecido pelos Sistemas Operacionais, ou por bibliotecas de algumas linguagens de programação.
1. Overview sobre Multithreads
1.1 O que são Threads?
Thread é uma forma de um processo dividir a si mesmo em duas ou mais tarefas, podendo executar elas concorrentemente. O suporte a threads é oferecido pelos Sistemas Operacionais, ou por bibliotecas de algumas linguagens de programação
1.2 O que são Aplicações Multithreads?
Um programa single - thread (thread única) inicia na etapa 1 e continua seqüencialmente (etapa 2, etapa 3, o passo 4) até atingir a etapa final. Aplicações multithread permitem que você execute várias threads ao mesmo tempo, cada uma executando um passo por exemplo.
Cada thread é executada em seu próprio processo, então, teoricamente, você pode executar o passo 1 em uma thread e, ao mesmo tempo executar o passo 2 em outra thread e assim por diante. Isso significa que a etapa 1, etapa 2, etapa 3 e etapa 4 podem ser executadas simultaneamente.
Na teoria, se todos os quatro passos durassem o mesmo tempo para executar, você poderia terminar o seu programa em um quarto do tempo que levaria para executar os quatro passos sequencialmente.
Então, por que todos os programas não são multithread? Uma das razões nos diz que junto com a velocidade vem a complexidade. Controlar um grande número de threads requer bastante trabalho e conhecimento. Imagine se um passo de alguma forma dependesse das informações no passo 2. O programa pode não funcionar corretamente se o passo 1 termina cálculo antes do passo 2 ou vice-versa.
Para ajudar entender essa complexidade, vamos fazer uma analogia comparando um programa multithread com o corpo humano. Cada um dos órgãos do corpo (coração, pulmões, fígado, cérebro) são processos e cada processo é executado simultaneamente. Assim, o corpo humano seria como uma grande aplicação multithread. Os órgãos são os processos em execução simultânea, eles dependem uns dos outros, sendo que todos os processos se comunicam através de sinais nervosos e fluxo de sangue, desencadeando uma série de reações químicas. Como em todos os aplicativos multithread, o corpo humano é muito complexo. Se algum processo não obtiver informações de outros processos, ou um processo retardar ou acelerar, vamos acabar com um problema médico. É por isso que, esses processos precisam ser sincronizados corretamente para funcionar normalmente.
1.3 Quando Utilizar Multithread?
Quando precisamos de desempenho e eficiência. Por exemplo, vamos pensar em um jogo de vídeo game. Imagine um jogo possuindo uma única thread, tanto para o processamento de imagens quanto para o processamento de áudio. Seria possível? Sim, seria. Mas esse é o cenário ideal para garantir a performance? Não. O ideal seria criar threads para o processamento das rotinas de imagens e outra para rotinas de áudio.
Outro cenário comum onde você precisaria de Multithread seria um sistema de tratamento de mensagens. Imagine um aplicativo que captura milhares de mensagens simultaneamente. Você não pode capturar eficientemente uma série de mensagens ao mesmo tempo em que você está fazendo algum outro processamento pesado, porque senão você pode perder mensagens. Então cabe aqui dividir o processamento de captura e processamentos paralelos em threads diferentes.
Cada um destes cenários são usos comuns para multithread e se utilizado de maneira correta, essa técnica vai melhorar significativamente o desempenho de aplicações similares.
Em linhas gerais, os benefícios do uso das threads advém do fato do processo poder ser dividido em várias threads; quando uma thread está à espera de determinado dispositivo de entrada/saída ou qualquer outro recurso do sistema, o processo como um todo não fica parado, pois quando uma thread entra no estado de 'bloqueio', uma outra thread aguarda na fila.
1.4 Quando não utilizar Multithread?
Geralmente quando aprendemos os recursos de multithread, ficamos fascinados com todas as possibilidades que essa técnica nos oferece e saímos usando – a em todos os tipos de programas que vemos pela frente.
É preciso tomar muito cuidado com isso, pois ao contrário de um programa single thread, você está lidando com muitos processos ao mesmo tempo, com múltiplas variáveis ??dependentes. Isso pode se tornar muito complicado de se controlar. Pensemos em multithread, um malabarismo. Malabarismos com uma única bola em sua mão (embora meio chato) é bastante simples. No entanto, se você é desafiado a colocar duas dessas bolas no ar, a tarefa é um pouco mais difícil. Imagine três, quatro, cinco bolas então, e as coisas vão se tornar cada vez mais difíceis. Como a contagem de bola aumenta você tem chances cada vez maiores de deixar cair alguma bola.
Trabalhar com multithreads requer muito trabalho e concentração. É preciso ressaltar que o uso dessa técnica em sistemas simples, geralmente não nos leva a um cenário muito proveitoso. Mesmo que o programador seja um cara muito bom e desenhe direitinho o sistema utilizando threads, fatalmente o ganho em performance não irá compensar caso seja necessário dar manutenções futuras no sistema.
2. Como Utilizar Threads
2.1 Criando uma Multithread
Dentro do .Net, para criar uma thread devemos utilizar o Namespace System.Threading. Nesta Namespace se encontra a classe Thread, que é a que usamos para instanciar uma thread.
No exemplo abaixo, veremos como instanciar uma thread e como rodá – la. Antes de tudo, uma thread sempre realiza determinada tarefa codificada em um método. Sendo assim, primeiramente vamos definir um método estático que realizará alguns prints em uma aplicação de Console:
Lembrando que todos os exemplos citados aqui foram feitos em C#, no Visual Studio 2008, utilizando um projeto do tipo Console Application.
public static void run()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Thread Atual - " + Thread.CurrentThread.Name + i);
Thread.Sleep(1000);
}
}
Este método apenas escreve na tela o nome da thread atual dentro de um loop for.
Repare que no código abaixo este método será utilizado para instanciar uma thread, pois para criar threads, precisamos apontar o método que ela executará, passando como parâmetro ao construtor da thread um objeto ThreadStart, que recebe o nome do método a ser executado pela thread.
Agora no método Main do seu Console Application, digite as seguintes linhas de código:
static void Main(string[] args)
{
Console.WriteLine("Thread principal iniciada");
Thread.CurrentThread.Name = "Principal - ";
Thread t1 = new Thread(new ThreadStart(run));
t1.Name = "Secundária - ";
t1.Start();
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Thread atual - " + Thread.CurrentThread.Name + i);
Thread.Sleep(1000);
}
Console.WriteLine("Thread Principal terminada");
Console.Read();
}
No código acima vemos como instanciar uma thread e como iniciá-la:
Thread t1 = new Thread(new ThreadStart(run));
t1.Name = "Secundária - ";
t1.Start();
Depois fazemos o mesmo loop do método run, dentro do método main para que os dois métodos rodem em concorrência.
Observe a chamada do método Sleep da classe Thread. Esse método suspende a execução da thread no período especificado como parâmetro em milissegundos.
Ao executar nosso Console Application temos o seguinte output:
2.2 Sincronização de Threads
O maior problema ao usar múltiplas threads é organizar o código de forma a dividir racionalmente o uso de recursos que elas venham a compartilhar. Esses recursos podem ser variáveis, conexões com banco de dados, dispositivos etc. Essa forma de organizar o acesso aos recursos compartilhados se chama Sincronização de Threads. Caso nós não sincronizarmos corretamente as threads, fatalmente cairemos nos famigerados deadlocks.
Para realizarmos uma boa sincronização de threads, devemos conhecer alguns métodos e propriedades de uma thread:
Métodos:
- Suspend(): Suspende a execução de uma Thread, até o método Resume() seja chamado.
- Resume(): Reinicia uma thread suspensa. Pode disparar exceções por causa de possíveis status não esperados das threads.
- Sleep(): Uma thread pode suspender a si mesma utilizando esse método que espera um valor em Milisegundos para especificar esse tempo de pausa.
- Join(): Chamado por uma thread, faz com que outras threads espere por ela até que ela acabe sua execução.
- CurrentThread(): Método estático que retorna uma referência à thread em execução
Estados de uma Thread:
Os estados podem ser obtidos usando a enumeration ThreadState, que é uma propriedade da classe Thread. Abaixo seguem os valores dessa enumeration:
- Aborted: Thread já abortada;
- AbortRequested: Quando uma thread chama o método Abort();
- Background: Rodando em bnackground;
- Running: Rodando depois que outra thread chama o método start();
- Stopped: Depois de terminar o método run() ou Abort();
- Suspended: Thread Suspendida depois de chamar o método Suspend();
- Unstarted: Thread criada mas não iniciada.
- WaitSleepJoin: Quando uma thread chama Sleep() ou Join(), ou quando uma outra thread chama join();
Propriedades:
- Name: Retorna o nome da thread;
- ThreadState: Retorna o etado de uma thread;
- Priority: Retorna a prioridade de uma thread. As prioridades podem ser:
- Highest
- AboveNormal
- Normal
- BelowNormal
- Lowest
- IsAlive: Retorna um valor booleano indicando se uma thread esta “viva” ou não;
- IsBAckground: Retorna uma valor booleano indicando se a thread está rodando em Background ou Foreground;
2.2.1 Sincrinizando Threads
Quando temos várias threads que compartilham recursos, precisamos fornecer acesso sincronizado a esses recursos. Temos de lidar com problemas de sincronização relacionados com o acesso simultâneo a variáveis ??e objetos acessíveis por múltiplas threads ao mesmo tempo. Para ajudar nesta sincronização, no .Net, podemos fazer com que uma thread bloqueie um recurso compartilhado enquanto o utiliza. Podemos pensar nisso como uma caixa onde o objeto está disponível e apenas um segmento pode entrar e outra thread só terá acesso ao recurso quando a outra que esta o utilizando saia da caixa.
Vamos exemplificar o uso de múltiplas threads. No mesmo Console Application, vamos primeiramente criar uma classe que conterá um método, que por sua vez será utilizado por algumas threads concorrentemente:
class Printer
{
public void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
Thread.Sleep(100);
Console.Write(i + ",");
}
Console.WriteLine();
}
}
No método Main da classe program, vamos instanciar um objeto da classe Printer, criar três threads passando o método PrintNumbers para cada thread criada, ou seja, as três threads tentarão utilizar o mesmo recurso:
static void Main(string[] args)
{
Console.WriteLine("======MultiThreads======");
Printer p = new Printer();
Thread[] Threads = new Thread[3];
for (int i = 0; i < 3; i++)
{
Threads[i] = new Thread(new ThreadStart(p.PrintNumbers));
Threads[i].Name = "threadFilha " + i;
}
foreach (Thread t in Threads)
t.Start();
Console.ReadLine();
}
Ao executar o programa temos o seguinte resultado:
Podemos ver como o Thread Scheduler nativo está chamando o objeto Printer para imprimir os valores numéricos. Como podemos ver temos um resultado inconsistente pois não temos qualquer tipo de controle de sincronização das threads, sendo que elas estão acessando o método PrintNumbers() de maneira aleatória.
Existem diversos meios de sincronização que podemos utilizar para garantir o processamento correto de nossos programas multithread.
A) Usando o Lock
Poderíamos por exemplo utilizar a pajavra-chave Lock dentro do método PrintNumbers():
public void PrintNumbers()
{
lock (this)
{
for (int i = 0; i < 5; i++)
{
Thread.Sleep(100);
Console.Write(i + ",");
}
Console.WriteLine();
}
}
A palavra-chave lock obriga-nos a especificar um token (uma referência de objeto) que deve ser adquirido por uma thread para entrar no escopo do lock (bloqueio). Quando estamos tentando bloquear um método em nível de instância, podemos simplesmente passar a referência a essa instância. (Nós podemos usar esta palavra-chave para bloquear o objeto atual) Uma vez que a thread entra em um escopo de bloqueio, o sinal de trava (objeto de referência) é inacessível por outros segmentos, até que o bloqueio seja liberado ou o escopo do bloqueio seja encerrado.
Execute o programa e você terá a seguinte tela:
B) Utilizando Monitor Type
Método Monitor.Enter () é o destinatário final do token da thread. Precisamos escrever todo o código do escopo de bloqueio dentro de um bloco try. A cláusula finally assegura que o token de thread seja liberada (usando o método Monitor.Exit () , independentemente de qualquer exceção de runtime.
Na classe Printer, modifique o método PrintNumbers como a seguir:
public void PrintNumbers()
{
Monitor.Enter(this);
try
{
for (int i = 0; i < 5; i++)
{
Thread.Sleep(100);
Console.Write(i + ",");
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Monitor.Exit(this);
}
}
Ao rodar o programa temos o seguinte resultado:
Vimos o que são threads, para que serve, quando utilizar, quando não utilizar e vimos alguns exemplos bem simples de utilização de múltiplas threads.
Peço aos leitores que deixem sempre seus comentários, principalmente comentários que apontem o que poderia ser melhorado neste artigo.
Espero que seja útil.
Obrigado.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo