Controlando Threads com Semáforos (C#)

Neste artigo irei abordar como podemos controlar as nossas threads em C#, seqüência de execução e controlar o número de threads que podemos executar, por vez, em nossa aplicação .

Para isso vou falar da classe Semáforo, namespace System.Threading.Semaphore.

Primeiro vamos estudar um pouco esta classe.

Podemos usar a classe semáforo para controlar um pool de threads em nossa aplicação, para isso existem 2 métodos que devemos entender, WaitOne() e Release().

WaitOne()

Este método deverá ser chamado logo no começo, para que a thread atual só entre em execução quando a outra thread estiver terminado.
Este método indica ao semáforo  que existe uma thread aguardando a execução e incrementa a contagem dentro do semáforo.

Release()

Este método deve ser chamado sempre que um thread for finalizada, ou quando se desejar liberar para que outra thread possa entrar em execução. Este método decrementa a contagem do semáforo;

 

Não existe uma garantia de que as threads serão executadas em FIFO (PEPS – Primeiro que Entra Primeiro que Sai) ou LIFO (UEPS – Ultimo que Entra Primeiro que Sai). Para isso temos que ter um controle das nossas threads.

A classe semáforo também não garante que ao chamar o método WaitOne() ou Release() a thread realmente possa ser executada.

Exemplo:

Se pegarmos duas threads X e Y, para a execução de Y temos que esperar X terminar. Caso haja algum erro em X e o método Release() não for chamado a thread Y nunca será executada.

Outro caso:

Para a execução de Y temos que esperar a thread X terminar, mas chamamos o método Release() logo no começo da thread e não no final. Isto fará com que a thread Y seja executada mesmo antes do término da thread X.

Outro erro que podermos ter é, se definirmos o nosso semáforo com o tamanho de três threads por execução e chamarmos o método Release() por mais de três vezes a exceção SemaphoreFullException será lançada.

Bom, chega de conversa. Vamos ver um código de exemplo.

No código abaixo irei mostrar como chamar as threads em ordem e aleatórias.

using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;

public class ExemploSemaforo
{
    //vamos usar um semáforo para limitar as threads.
    private static Semaphore Pool;

    //este flag é apenas para indicar que estamos na thread desordenada.
    // então temos que chamar o waitOne nos métodos
    private static bool flag = false;

    public static void Main()
    {
        Console.WriteLine("Programa Iniciado");

        ThreadOrdenada();

        ThreadAleatoria();

        Console.WriteLine("Aguarde as threads serem executadas");

        Console.ReadKey();
    }

    /// <summary>
    /// este método irá executar as threads em ordem de criação
    /// </summary>
    private static void ThreadOrdenada()
    {
        Console.WriteLine("Threads Ordenadas");

        //este buffer será criado para garantir que as threads
        //sejam executadas na ordem em que foram chamadas (criadas)
        //iremos usar ele mais abaixo para iniciar as nossas threads
        Queue<Thread> ThreadBuffer = new Queue<Thread>();

        //iniciamos o Pool

        //o primeiro parâmetro do Pool indica quantas threads temos liberadas para iniciar
        //o segundo parâmetro indica quantas threads podemos executar por vez
        Pool = new Semaphore(0, 1);

        //criando um Pool de threads que chamam o MetodoUm
        for (int i = 0; i < 5; i++)
        {
            Thread t = new Thread(new ThreadStart(MetodoUm));
            t.Name = "MetodoUm_Thread#" + i;
            ThreadBuffer.Enqueue(t);
        }

        //criando um Pool de threads que chamam o MetodoDois
        for (int i = 0; i < 5; i++)
        {
            Thread t = new Thread(new ThreadStart(MetodoDois));
            t.Name = "MetodoDois_Thread#" + i;
            ThreadBuffer.Enqueue(t);
        }

        //agora iremos iniciar todas as threads
        while (ThreadBuffer.Count > 0)
        {
            Thread t = ThreadBuffer.Dequeue();
            t.Start();

            //espera o fim da thread anterior para continuar
            //resolvi esperar aqui para garantir que qualquer thread que for executada
            //irá esperar a outra terminar.
            //se tirarmos esta linha as threads nao terão uma ordem definida para executar.
            Pool.WaitOne();//comente este linha e execute o programa novamente. Atente aos erros
        }
    }

    /// <summary>
    /// Este método chama as threads em qualquer ordem
    /// </summary>
    private static void ThreadAleatoria()
    {

        Console.WriteLine("Threads Aleatórias");

        flag = true;

        //iniciamos o Pool

        //o primeiro parâmetro do Pool indica quantas threads temos liberadas para iniciar
        //o segundo parâmetro indica quantas threads podemos executar por vez

        //repare que aqui, diferente da thread ordenada
        //o primeiro parâmetro é 3 para indicar que temos posições livres
        //o segundo é três para indicar que podemos executar até 3 por vez
        Pool = new Semaphore(3, 3);

        //criando um Pool de threads que chamam o MetodoUm
        for (int i = 0; i < 5; i++)
        {
            Thread t = new Thread(new ThreadStart(MetodoUm));
            t.Name = "MetodoUm_Thread#" + i;
            t.Start();
        }

        //criando um Pool de threads que chamam o MetodoDois
        for (int i = 0; i < 5; i++)
        {
            Thread t = new Thread(new ThreadStart(MetodoDois));
            t.Name = "MetodoDois_Thread#" + i;
            t.Start();
        }
    }

    private static void MetodoUm()
    {
        try
        {
            if (flag) Pool.WaitOne();

            Console.WriteLine("A thread " + Thread.CurrentThread.Name + " iniciou.");
            //espera 3 segundos antes de terminar
            Thread.Sleep(3000);

            if (Thread.CurrentThread.Name == "MetodoUm_Thread#1")
                throw new Exception("erro apenas para mostrar que não podemos esquecer de liberar o semáforo mesmo \nque haja erros...");

            Console.WriteLine("A thread " + Thread.CurrentThread.Name + " finalizou.");
        }
        catch (Exception ex)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(ex.Message);
            Console.ResetColor();
        }
        finally
        {
            //temos que sinalizar que a thread terminou.
            //isso é muito importante, não podemos esquecer, pois caso contrário
            //as outras threads não serão executadas
            Pool.Release();
        }
    }

    private static void MetodoDois()
    {
        try
        {
            if (flag) Pool.WaitOne();

            Console.WriteLine("A thread " + Thread.CurrentThread.Name + " iniciou.");
            //espera 3 segundos antes de terminar
            Thread.Sleep(3000);
            Console.WriteLine("A thread " + Thread.CurrentThread.Name + " finalizou.");

        }
        catch (Exception ex)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(ex.Message);
            Console.ResetColor();
        }
        finally
        {
            //temos que sinalizar que a thread terminou.
            //isso é muito importante, não podemos esquecer, pois caso contrário
            //as outras threads não serão executadas
            Pool.Release();
        }

    }
}

É isso ai pessoal 🙂
Até o próximo
♦ Marcelo

Marcelo

Nascido em Juruaia/MG em uma fazenda de criação de búfalos, e residindo na região Sul do Brasil.
Trabalha com desenvolvimento de aplicações desde os 17 anos. Atualmente é Arquiteto Organizacional na Unimake Software.
Para saber mais ... http://desenvolvedores.net/marcelo
[]'s

Você vai gostar de...

Postagens populares.

9 Comments

  1. É possível fazer com que cada thread utilize variáveis sem compartilhar seu valor ?

    1. Utilize o lock, acredito que resolve seu problema.


      static Readonly Object _lock=new Object();
      lock(_lock)
      {
      //faça algo
      }

  2. Nesse seu exemplo eu consigo utilizar ele de forma que eu puxe uma letra de um documento usando o Fopen ?

    Abraços

    1. Estamos falando de C#, e sim, você consegue fazer qualquer coisa entre as threads.

      1. Como devo proceder no visual studio?

        1. Explique melhor a sua dúvida, por favor …

  3. Quanto ao primeiro parâmetro:
    Imagine que você declare um semáforo com 5 threads de limite (segundo parâmetro).
    No primeiro parâmetro você pode indicar quantas requisições estão livres para poder iniciar.
    Ao chamar o WaitOne(), ele irá verificar a contagem de requisições livres. No primeiro parâmetro, se você indicar 3, seria o mesmo que você tivesse feito 3x a chamada ao método Release().
    Isso faria com que fosse executado 3 threads ao mesmo tempo, pois o WaitOne() verificaria que ainda existem 3 requisições livres.

    A sua dúvida quanto a eu colocar 0 (zero) no primeiro parâmetro, é que naquele momento eu não quero iniciar o semáforo com nenhuma requisição inicial. Pois vou controlar isso mais abaixo, para ordenar as execuções das threads.

    1. Parabéns pelo POST e obrigado por responder minha dúvida, agora entendi perfeitamente.

  4. Este POST ficou Show de boa, super fácil de compreender e bem prático. Só fiquei com dúvida quanto ao primeiro parâmetro do construtor da classe Semaphore(3,3). Eu não consegui alcançar o que você quis dizer com “o primeiro parâmetro do Pool indica quantas threads temos liberadas para iniciar” e “//o primeiro parâmetro é 3 para indicar que temos posições livres”, em uma delas você coloca Zero e na outra três. Se ela determina quantas threads temos livre para execução, pq na primeira está Zero? Poderia me explicar melhor a idéia?

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.