Canais iMasters

C# + .NET

Sincronização de Threads em C#

Um aviso aos leitores deste artigo: boa parte do conteúdo é uma tradução do artigo “Thread Synchronization (C# and Visual Basic)” disponibilizado pela Microsoft. Aproveite a leitura!

Um dos benefícios do uso de múltiplas threads em uma aplicação é que cada uma delas é executada de forma assíncrona. Em aplicações desktop, isto permite que tarefas que consomem muito tempo possam ser executada em segundo plano enquanto a janela do aplicativo e os controles continuam respondendo. 

Para aplicações em servidores, múltiplas tarefas fornecem a capacidade de lidar com cada solicitação de entrada com uma thread diferente, caso contrário, cada novo pedido não será atendido até que o pedido anterior tenha completado.

No entanto, a natureza assíncrona de threads significa que o acesso a recursos compartilhados, como arquivos, conexões de rede e memória devem ser sincronizados. Caso contrário, duas ou mais threads podem acessar o mesmo recurso ao mesmo tempo, e cada um desconhece a atuação da outra ação. O resultado é a corrupção de dados imprevisível e possíveis deadlocks.

Há algum tempo, fiz outros artigos que demonstram o uso de threads em C# e Visual Basic. Você pode encontrá-los aqui:

Lock e SyncLock

O Lock (C#) e o SyncLock (VB.NET) são declarações que podem ser utilizadas para garantir que um bloco de código seja executado até sua conclusão, prevenindo a interrupção da execução por outras threads.

Essas declarações são interpretadas pelo compilador como bloqueios para um determinado objeto em um bloco de códigos. Se outra thread tenta executar o código envolvido pelo lock/SyncLock, ela evai esperar até que a primeira thread termine a execução e libere o objeto/código bloqueado.

//C#
public class LockTest
{
public void TestarLock()
{
lock (this)
{
// Código que estará protegido pelo lock.
}
}
}

Sincronização de eventos e Wait Handles

Existem dois tipos de sincronização: AutoResetEvent e ManualResetEvent. Eles diferem apenas na medida em que as mudanças são setadas automaticamento no AutoResetEvent e manualmente no ManualResetEvent. O ManualResetEvent permite que qualquer número de threads possa ser alinhado e ter seu estado atualizado.

Métodos são usados para esperar a execução de um ou mais threads. Esses métodos indicam ao compilador quando e quantas threads devem ter o processamento concluído para que o fluxo continue. São eles: WaitOne, WaitAny e WaitAll.

WaitOne faz com que o compilador espere a execução de um único thread estar concluída. WaitAny faz o compilador aguardar a execução dos métodos indicados. WaitAll bloqueia a execução até que todas as threads estejam concluídas. Para sinalizar a conclusão da execução de uma thread, usa-se o método Set.

// C#
namespace GerenciaSincronizacao
{
public class GerenciarThreads
{
private ManualResetEvent[] manualResetEvent;

private void ExecutarThread0(object obj)
{
Thread.Sleep(20000);
Console.WriteLine("Thread 0 concluída!");
manualResetEvent[0].Set();
}

private void ExecutarThread1(object obj)
{
Thread.Sleep(13000);
Console.WriteLine("Thread 1 concluída!");
manualResetEvent[1].Set();
}

private void ExecutarThread2(object obj)
{
Thread.Sleep(9000);
Console.WriteLine("Thread 2 concluída!");
manualResetEvent[2].Set();
}

private void ExecutarThread3(object obj)
{
Thread.Sleep(17000);
Console.WriteLine("Thread 3 concluída!");
manualResetEvent[3].Set();
}

public void Executar()
{
manualResetEvent = new ManualResetEvent[4];

manualResetEvent[0] = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(this.ExecutarThread0);

manualResetEvent[1] = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(this.ExecutarThread1);

manualResetEvent[2] = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(this.ExecutarThread2);

manualResetEvent[3] = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(this.ExecutarThread3);

WaitHandle.WaitAll(manualResetEvent);
}
}

class Program
{
static void Main(string[] args)
{
GerenciarThreads gt = new GerenciarThreads();
gt.Executar();

Console.WriteLine("Pressione ENTER para terminar.");
Console.ReadKey();
}
}
}

A classe Interlocked

Você pode usar os métodos da classe Interlocked para evitar problemas que podem ocorrer quando várias threads tentam simultaneamente atualizar ou comparar um mesmo valor. Os métodos dessa classe permitem que você assegure o incremento, decremento, troca e comparação entre valores em qualquer thread.

As operações mais utilizadas desta classe são:

  • O método Add adiciona um valor inteiro para uma variável e retorna o novo valor da variável.
  • O método Read lê um valor inteiro de 64 bits.
  • Os métodos Increment e Decrement são utilizados para incrementar ou decrementar valores em uma variável e retornam o valor resultante.

O código a seguir demonstra o uso do Increment:

// C#
private static ManualResetEvent[] manualResetEvents;

public void Teste()
{
int intQuantidadeInteracoes = 10;
int intContador = 0;

manualResetEvents = new ManualResetEvent[intQuantidadeInteracoes];

for (int i = 0; i < intQuantidadeInteracoes; i++)
{
manualResetEvents[intContador] = new ManualResetEvent(false);

ThreadPool.QueueUserWorkItem(this.Executar, intContador);

Interlocked.Increment(ref intContador);
}

WaitHandle.WaitAll(manualResetEvents);
}

public void Executar(object intContador)
{
Console.WriteLine("Posição atual do contador: {0}",
Convert.ToInt32(intContador));

manualResetEvents[Convert.ToInt32(intContador)].Set();
}

Referências


Comente também

2 Comentários

Fábio Kiatkowski
Fábio Kiatkowski

Muito bom, não existe melhor momento pra este artigo aparecer...
simplesmente muito bom mesmo...

Parabéns

Willian Rodrigues de Moura
Willian Rodrigues de Moura

Não use "lock (this)", sempre use algum outro objeto para fazê-lo. Um método simples e correto de se fazer é declarar um objeto somente leitura e público, exemplo: "public readonly object syncRoot = new object();" e usar "lock (this.syncRoot)" ou em caso de outra instância "lock (other.syncRoot)".

Usar o "lock (this)" pode acarretar impasses (dead-locks), e alguns afirmam que quanto maior for um objeto, maior será o gasto para travá-lo.

Uma outra forma mais simples de se evitar problemas de múltiplos acessos a uma variável é declará-la como "volatile", exemplo: "public volatile int _count;", se assim declarado, não se faz necessidade de tratar por locks ou waits.

Qual a sua opinião?

Comentários considerados ofensivos serão moderados.

Parceiros

IBM
PagSeguro
Internet Innovation
Dialhost
HostNet
Tecla
KingHost
DotStore
Dinamize