C# - Trabalhando com Threads


Neste artigo volto a falar sobre threads desta vez usando a linguagem C#.

O que é uma thread (Linha de execução) ?

Vejamos algumas definições:

Linha de execução (em inglês: Thread), é uma forma de um processo dividir a si mesmo em duas ou mais tarefas que podem ser executadas concorrentemente. O suporte à thread é fornecido pelo próprio sistema operacional (SO), no caso da linha de execução ao nível do núcleo (em inglês: Kernel-Level Thread (KLT)), ou implementada através de uma biblioteca de uma determinada linguagem, no caso de uma User-Level Thread (ULT).

Uma linha de execução permite que o usuário de programa, por exemplo, utilize uma funcionalidade do ambiente enquanto outras linhas de execução realizam outros cálculos e operações.
http://pt.wikipedia.org/wiki/Thread_(ci%C3%AAncia_da_computa%C3%A7%C3%A3o)
"Uma Thread é Um fluxo de controle sequencial isolado dentro de um programa"
Como um programa sequencial qualquer, um thread tem um começo, um fim, e uma seqüência de comandos. Entretanto, um thread não é um programa, não roda sozinho, roda dentro de um programa.
Threads permitem que um programa simples possa executar várias tarefas diferentes ao mesmo tempo, independentemente umas das outras. Programas multi-threaded são programas que contém vários threads, executando tarefas distintas, ao mesmo tempo.

 

Threads, sequências, linhas de execução, encadeamento não importa o nome , todas as aplicações são executadas em uma thread principal.
 

Uma aplicação pode ter mais de uma thread ao mesmo tempo, ou seja, podemos estar fazendo várias coisas ao mesmo tempo, quer um exemplo ? O sistema operacional Windows. A barra de tarefas exibe os processos que estão sendo executados simultaneamente.
 

Não se iluda , embora você esteja vendo a barra de tarefas (Task Manager) exibir diversos aplicativos sendo executados , nenhum deles esta sendo executado ao mesmo tempo. Não existe computador que possa realizar esta proeza com uma única CPU. O que o Windows , e qualquer outro aplicativo que suporte a multitarefa , faz é estar alternando rapidamente entre diferentes threads de forma que cada thread pensa que esta executando independentemente, mas, só executa por algum tempo, interrompe e depois volta, e assim por diante.

A linguagem C# suporta a execução paralela de código através do multithreading onde uma Thread é uma caminho de execução independente que esta apto para rodar simultaneamente com outras threads.

Um programa C# quer do tipo Console, WPF ou Windows Forms inicia em uma única thread criada de forma automática pela CLR (Common Languagem Runtime) e pelo Sistema Operacional ( a thread principal - main ) e torna-se multithread pela criação de threads adicionais.

Vamos iniciar criando um thread simples na linguagem C# . Estou usando o Visual C# 2010 Express Edition e em todos os exemplos estou usando o namespace:

using System.Threading;

Abra o Visual C# 2010 Express e crie um novo projeto do tipo Console chamado UsandoThreads_1 com o seguinte código:

using System;
using System.Threading;

namespace UsandoThreads_1
{
    class Program
    {
        static void Main(string[] args)
        {
          
 // dispara uma nova thread para executar
            Thread t = new Thread(NovaThread);         
            t.Start();                              

         
  // Simultaneamente, executa uma tarefa na thread principal
            for (int i = 0; i < 10000; i++) Console.Write("1");
        }

        static void NovaThread()
        {
            for (int i = 0; i < 10000; i++) Console.Write("2");
        }
    }
}

A Neste singelo programa a thread principal cria uma nova thread t a qual roda um método que imprime na tela o numero 2 (segunda thread);

Simultaneamente a thread principal imprime o número 1 (thread principal).

Observe que a thread principal inicia e já cria a segunda thread que começa a imprimir o número 2, após isso a execução é intercalada e ocorre simultaneamente exibindo o resultado da figura mostrada ao lado do código.

O método Start() inicia a execução da segunda thread e após o término da execução ambas as threads são encerradas.

Abaixo temos uma figura que resume o comportamento da execução do programa:

Uma vez iniciada pelo método Start() a propriedade IsAlive da thread retorna true até que a thread seja encerrada.

Nota: A propriedade IsAlive obtém um valor que indica o status de execução do thread corrente. O valor true indica que a thread foi iniciada e não foi terminada.

Uma thread termina quando o delegate passado para o construtor da thread encerra a execução e uma vez terminada uma thread não pode ser re-iniciada.

A CLR atribui a cada thread sua própria alocação de memória na pilha (Stack) de forma que variáveis locais são mantidas separadas.

Vejamos agora um exemplo onde definimos um método com uma variável local e então chamamos o método simultaneamente na thread principal e na nova thread criada;

Crie um novo projeto do tipo console com o nome UsandoThreads_2 e defina o seguinte código no programa:

using System;
using System.Threading;

namespace UsandoThreads_2
{
    class Program
    {
        static void Main(string[] args)
        {
          
 // Chama NovaThread em uma nova Thread
            new Thread(NovaThread).Start();     
       
    // Chama NovaThread na thread principal
            NovaThread();
            Console.ReadKey();
        }

        static void NovaThread()
        {
          
 //Declara e usa a variável local - contador
            for (int contador = 0; contador < 5; contador++)
                  Console.Write('2');
        }
    }
}
Neste código estamos criando uma nova thread e chamando a rotina NovaThread;

Em seguida chamamos novamente a rotina NovaThread na thread principal;

A rotina NovaThread declara e usa uma variável local chamada contador;

Uma cópia separada da variável contador é criada e cada thread aloca a área de memória
Stack para a variável;

Dessa forma o resultado da execução é o seguinte:

A threads podem compartilhar dados se eles tiverem uma referência comum para a mesma instância. Veja o exemplo a seguir:

Crie um novo projeto do tipo console com o nome UsandoThreads_3 e defina o seguinte código no programa:

using System;
using System.Threading;

namespace UsandoThreads_3
{
    class Program
    {
        bool ok;

        static void Main(string[] args)
        {
          
 // Cria uma instância comum
            Program tt = new Program();  
           
//inicia a execução da rotina na nova instância
            new Thread(tt.NovaThread).Start();
         
  //executa a rotina na mesma instãncia
            tt.NovaThread();
        }

    
   // Note que NovaThread é agora um método de instãncia
        void NovaThread()
        {
            if (!ok)
            {
                ok = true;
                Console.WriteLine("www.macoratti.net");
            }
        }
    }
}
Agora estamos criando uma instância comum , iniciando uma nova thread,
onde estamos executando a rotina NovaThread, e, em seguida executamos
novamente a mesma rotina na mesma instância.

Como estamos usando a mesma instância iremos obter o mesmo resultado
conforme exibido a seguir:

Uma outra forma de compartilhar dados entre as threads é definir campos estáticos. Veja um exemplo no código a seguir:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace UsandoThreads_4
{
    class Program
    {
        static bool ok;

        static void Main(string[] args)
        {
            new Thread(NovaRotina).Start();
            NovaRotina();
            Console.ReadKey();
        }

        static void NovaRotina()
        {
            if (!ok)
            {
                ok = true;
                Console.WriteLine("www.macoratti.net");
            }
        }

    }
}
Executando este código o resultado será o mesmo que o exemplo anterior,

Se alterarmos a ordem das instruções na rotina NovaRotina();

static void NovaRotina()
{
  if (!ok)
  {
    Console.WriteLine(
"www.macoratti.net");
    ok = true;
  }
}

Poderemos obter após algumas execuções o resultado a seguir:

Mas o que esta acontecendo ???

O problema é que uma thread pode estar avaliando a instrução if enquanto a outra thread
pode estar executando a instrução Writeline.

Temos aqui o problema : as nossas threads não são Thread Safety. Como ????

Thread safety é um conceito existente no contexto de programas
multi-threads onde um trecho de código é thread-safe se :
ele funcionar corretamente durante execução simultânea por vários threads.

Se o código não for thread-safe e estiver rodando em um ambiente multi-threads os resultados obtidos podem ser inesperados ou incorretos pois uma thread pode desfazer o que a outra thread já realizou. Esse é o problema clássico da atualização do contador em uma aplicação multi-thread.

Para resolver este problema podemos usar a instrução lock() que vai bloquear os recursos até que a thread que o esta usando acabe de processá-lo.

O lock funciona assim : Quando duas threads simultâneas contêm um lock() ou bloqueio, uma thread espera, até que o bloqueio seja liberado. Neste caso, somente uma thread terá acesso ao recurso por vez e dessa forma torná-lo thread-safe.

No exemplo acima podemos usar o bloqueio ou lock da seguinte forma:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace UsandoThreads_4
{
    class Program
    {
        static bool ok;
        static readonly object bloqueador  = new object;

        static void Main(string[] args)
        {
            new Thread(NovaRotina).Start();
            NovaRotina();
            Console.ReadKey();
        }

        static void NovaRotina()
        {
          lock(bloqueador)
          {
            if (!ok)
            {
                Console.WriteLine(
"www.macoratti.net");
                ok = true;
            }
         }

    }
}

Enquanto estiver bloqueada uma thread não consome recursos do sistema.

Você também pode esperar que uma outra thread termine a sua execução usando o método Join(). Veja o exemplo:

using System;
using System.Threading;

namespace UsandoThreads_5
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread macoratti = new Thread(Rotina);
     
      //inicia a thread
            macoratti.Start();
          
 //aguarda o término da thread
            macoratti.Join();
            Console.WriteLine("\r\n\r\nA Thread macoratti terminou a sua execução!");
            Console.ReadKey();
        }
     
  static void Rotina()
        {
            for (int i = 0; i < 1000; i++)
                Console.Write("www.macoratti.net");
        }

    }
}
A execução do código imprime 1000 vezes a frase www.macoratti.net e a seguir o
texto para indicar que a thread terminou:

Obs: \r\n -> é igual e Environment.NewLine e causa uma avançao para outra linha.

Você pode incluir um tempo de espera quando faz a chamada do Join em
milisegundos ou como um TimeSpan que retornará true se a thread já
terminou ou false se o tempo expirou.

Para incluir um tempo de espera, pausar uma thread, usamos o método Sleep();

O método Sleep() é um método estático que recebe como parâmetro um valor que define o período de espera:

Thread.Sleep (TimeSpan.FromHours (1));  // pausa por 1 hora
Thread.Sleep (500);                                  // pausa por 500 milliseconds

Enquanto estiver esperando, quer seja através do método Sleep ou Join , a thread fica bloqueada e não consome recursos de CPU.

Se for definido o valor zero => Thread.Sleep(0) , a thread será suspensa para permitir que outras threads que estão aguardando o processamento sejam executadas.

Para bloquear uma thread indefinidamente utilize o valor Infinite => Thread.Sleep(infinite)

A partir da versão 4.0 do .NET Framework existe o método Yield => Thread.Yield() , que faz a mesma coisa, exceto pelo fato de que são liberadas somente threads que estão rodando no mesmo processo. O sistema operacional seleciona a thread que será executada.

Tanto Sleep() como Yield()são úteis para realizar testes de desempenho e diagnósticos para desvendar características thread-safety.

Neste ponto vamos fazer um resumo do que já vimos sobre Threads:

Métodos     Descrição
Start Faz com que uma thread seja agendado para execução.
Lock Bloqueia os recursos até que a thread que o esta usando acabe de processá-lo.
Join Bloqueia o segmento de chamada até que um thread seja encerrado.
Sleep Bloqueia o corrente thread pelo número especificado de milissegundos

Matando uma thread

Podemos matar uma thread diretamente chamando o método Abort para encerrar thread.

A chamada do método Abort faz com que a thread atual encerre a execução lançando a exceção: ThreadAbortException.

Para saber se uma thread esta viva consultamos sua propriedade IsAlive() a qual devolve true apenas se a thread foi iniciada e ainda não esta morta.

Uma thread pode estar em um dos seguintes estados:
Unstarted A thread foi criada mas não foi iniciada
Running A thread foi iniciada
WaitSleepJoin A thread espera por um
Suspended A thread foi suspensa
Stopped A thread parou ou foi abortada

Para encerrar esta introdução sobre threads em C# nada melhor que um exemplo funcional..

Usando Threads em aplicações Windows Forms

Vamos criar uma aplicação Windows Forms usando a linguagem C# que utiliza e gerencia threads.

Abra o Visual C# 2010 Express Edition e crie um novo projeto do tipo Windows Application com o nome DemoThreads;

- Adicione um controlo de botão ao formulário e altere sua propriedade Name para btnIniciar;
- Adicione um componente de ProgressBar ao formulário e altere sua propriedade Name para : pgbThreads;

O leiaute do formulário é bem simples e esta exibido a seguir:

- Clique com o botão direito do mouse no formulário e clique em Ver código;
- Defina o seguinte namespace no formulário:
using System.Threading;

- Defina no evento Click do botão de comando - btnIniciar o seguinte código;

 private void btnIniciar_Click(object sender, EventArgs e)
 {
            MessageBox.Show("Esta é a thread principal do programa");
 }

- Defina também a variável trd do tipo Thread no formulário;

private Thread trd;

Após isso vamos criar uma rotina chamada ThreadTarefa que vai desenhar a barra de progresso;

Este código é um laço infinito que aleatoriamente gera incrementos para diminui o valor pbgThreds e, em seguida, aguarda 100 mili-segundos antes de continuar.

private void ThreadTarefa()
{
     int stp;
     int novoValor;
     Random rnd=new Random();

     while(true)
      {
    stp=this.progressBar1.Step*rnd.Next(-1,2);
    novoValor = this.progressBar1.Value + stp;

    if (novoValor > this.progressBar1.Maximum)
novoValor = this.progressBar1.Maximum;
     else if (novoValor < this.progressBar1.Minimum)
novoValor = this.progressBar1.Minimum;

     this.progressBar1.Value = novoValor;

    Thread.Sleep(100);
       }
}

Finalmente no evento Load do formulário vamos definir a thread e iniciá-la:

     private void Form1_Load(object sender, EventArgs e)
     {
            Thread trd = new Thread(new ThreadStart(this.ThreadTarefa));
            trd.IsBackground = true;
            trd.Start();
     }

Ao executar o projeto você vai se assustar pois vai receber a seguinte mensagem de erro:

Esta exceção é lançada quando um controle em um formulário é acessado a partir de outra thread.

Mas então as threads não funcionam em projetos Windows Forms ???

Infelizmente trabalhar com threads sincronizadas para controles em aplicações Windows Forms é um problema que você terá que resolver sozinho pois não é possível trabalhar com objetos criados por outra thread a não ser usando um delegate.

A CLR - common language runtime fornece um mecanismo de callback, também chamado de delegate, o qual são equivalentes a ponteiros de funções em outras linguagens com a vantagem de possuirem um mecanismo type-safe.

Quando você declarar um delegate em seu código o compilador gera uma classe derivada da classe Delegate ou MulticastDelegate e a CLR implementa todos os métodos de interesse do delegate de forma dinâmica em tempo de execução.

Nota: A classe MulticastDelegate é uma classe abstrata que contém uma lista linkada de delegates conhecida como um lista de invocação.

Chamar um delegate é igual sintaticamente a chamar uma função, por isso, os delegates são perfeitos para implementar callbacks e eles fornecem um excelente mecanismo para desacoplar o método que esta sendo chamado em uma instância a partir do método chamador.

Se a solução é definir um delegate vamos a ele. Veja como fica o código para este exemplo:

Obs: Esta solução foi encontrada em shabdar.org

Temos que substituir a linha de código :  

this.pgbThreads.Value = novoValor; (esta linha sai)

pela seguinte linha de código:

SetControlPropertyValue(pgbThreads,"value",novoValor); (esta linha entra)

A seguir temos que definir o delegate com o seguinte código:

       delegate void SetControlValueCallback(Control oControl, string propName, object propValue);

        private void SetControlPropertyValue(Control oControl, string propName, object propValue)

        {

            if (oControl.InvokeRequired)

            {

                SetControlValueCallback d = new SetControlValueCallback(SetControlPropertyValue);

                oControl.Invoke(d, new object[] { oControl, propName, propValue });

            }

            else

            {

                Type t = oControl.GetType();

                PropertyInfo[] props = t.GetProperties();

                foreach (PropertyInfo p in props)

                {

                    if (p.Name.ToUpper() == propName.ToUpper())

                    {

                        p.SetValue(oControl, propValue, null);

                    }

                }

            }

        }

Para este código funcionar temos que incluir o seguinte namespace no projeto:

using System.Reflection;

Agora sim, se executarmos o projeto teremos um laço infinito onde são gerados valores aleatórios para o controle ProgressBar que é exibido no formulário;

Esta solução pode ser aplicada para qualquer controle Windows Forms e tudo o que você tem que fazer é copiar os delegate SetControlValueCallback  e a função SetControlPropertyValue e ajustar para o seu caso.

Assim se o problema estiver na utilização do controle TextBox basta fazer assim:

SetControlPropertyValue(Label1, "Text", i.ToString());

Tenha cuidado de fornecer os valores das propriedades com o tipo correto.

Quer outra solução ?

Dê uma espiada no meu artigo :

Executando procedures em Threads

Que mostra como fazer isso usando o novo controle BackgroundWorker.

Simples, simples assim...

Eu sei é apenas C# , mas eu gosto...

Pegue o projeto completo aqui: DemoThreads.zip

Referências:


José Carlos Macoratti