Desafio da Semana #13


 


Por: Roberto Alexis Farah


 


Olá pessoal! Estou em débito com meus leitores, pois já se passaram semanas e ainda não tive tempo de publicar um novo desafio do mês, ops…, digo, da semana.


No desafio de hoje começarei a abordar um tópico que ainda não foi parte dos desafios: problemas relacionados a multithreading.


Particularmente com Visual Basic .NET e C# é bastante fácil de se usar threads em aplicações, entretanto, sincronizar o acesso dessas threads a recursos compartilhados é outra história.


Muitas vezes quando trabalhando em incidentes de aplicações com multithread e sintomas usualmente classificados como hang ou performance, recebo perguntas de clientes como:


 


      Devo fazer o lock menos ou mais granular?


      Qual o melhor mecanismo de sincronização para usar em determinada situação?


      Porque minha aplicação roda com boa performance usando 25 threads, roda com mais performance com 50 threads mas roda lentamente com 100 threads?


      Porque minha aplicação multithread degrada a performance lentamente?


      Quando preciso e quando não preciso me preocupar com acesso sincronizado de múltiplas threads?


 


Portanto, começo por esse desafio a escrever sobre multithreading, continuando a contribuição para se evitar o uso de POG (Programação Orientada a Gambiarras) (isso é bem engraçado J )em qualquer contexto, principalmente em programação multithreading.


 


 


CENÁRIO


 


Temos uma aplicação C# abaixo, feita com .NET Framework 2.0 que pode ser facilmente convertida para VB.NET e o mesmo sintoma ocorrerá.


A aplicação usa um método em modo compartilhado que incrementa uma variável, entretanto, assuma que a operação poderia ser outra coisa diferente de incrementar uma variável.


 


using System;


using System.Collections.Generic;


using System.Text;


using System.Threading;


 


namespace ThreadBug


{


          public class Bacteria


          {


                   private static int _bacteriaCount;


                  


                   private static void DoSomething()


                   {


                             Console.WriteLine(“DoSomething chamado por {0} no momento {1}”,


                                                                      Thread.CurrentThread.ManagedThreadId,


                                                                     DateTime.Now.ToLongTimeString());


 


                             // Assuma que o método poderia fazer outra operação que não incrementar uma variável!                                                                     


                             _bacteriaCount++; 


                            


                             Thread.Sleep(3000);  


                   }


                  


                   public Bacteria()


                   {


                             lock(this)


                             {


                                      DoSomething();


                             }


                   }


          }


         


          public class SpecializedBacteria : Bacteria


          {


          }


         


         


          class Program


          {


                   private static void Worker1()


                   {


                             Console.WriteLine(“Worker1 executando thread {0}”,


                                                                     Thread.CurrentThread.ManagedThreadId);


                                                                            


                             Bacteria bac = new Bacteria();


                   }


 


                   private static void Worker2()


                   {


                             Console.WriteLine(“Worker2 executando thread {0}”,


                                                                     Thread.CurrentThread.ManagedThreadId);


 


                             SpecializedBacteria bac = new SpecializedBacteria();


                   }


                  


                   [STAThread]


                   static void Main(string[] args)


                   {


                             Thread thread1 = new Thread(new ThreadStart(Worker1));


                             Thread thread2 = new Thread(new ThreadStart(Worker2));


                            


                             Console.WriteLine(“Iniciando as threads em {0}”,


                                                       DateTime.Now.ToLongTimeString());


                             thread1.Start();


                             thread2.Start();


                            


                             Thread.Sleep(4000);


                   }


          }


}


 


 


SINTOMA


 


As threads acessam, ao mesmo tempo, o método DoSomething(). O correto é que apenas uma thread, em determinado momento, acessar o método estático.


 


OBJETIVO


 


Identifique o PROBLEMA ocasionando a falha na sincronização das threads e proponha uma SOLUÇÃO.


 


Nota: Assuma que o método poderia fazer uma operação diferente de incrementar uma variável, portanto, a solução deve contemplar essa possibilidade.


 


 


 


 

Comments (3)

  1. Marcondes says:

    Olá Farah!

    observando o código, gostaria então de postar minha identificação e solução do problema.

    PROBLEMA

    No trecho de código apresentado, existe um problema no construtor da classe Bacteria. Nesse construtor, o lock está sendo realizado na instância da classe. Porém, o método DoSomething é estático e está acessando um membro estático. Dessa forma, quando você cria duas instâncias de classe (uma de Bacteria e outra de SpecializedBacteria), e acessa o método DoSomething a partir de 2 threads, cada uma delas estará "lockando" cada instância independentemente, porém, acessando o mesmo membro estático em ambos os casos.

    Para resolver o problema, podemos fazer o seguinte:

    SOLUÇÃO

    Criamos um objeto de sincronismo chamado SyncRoot da seguinte forma. Ele literalmente é um objeto dummy que não nada a não ser servir de referência para lock.

    private class SyncRoot

    {

    }

    no código passado, fazendo algumas modificações teremos:

    public class Bacteria

             {

                      private static int _bacteriaCount;

                      private static SyncRoot _syncRoot = new SyncRoot();

                      private static void DoSomething()

                      {

                           lock(_syncRoot)

    {    

    Console.WriteLine("DoSomething chamado por {0} no momento {1}",

                                                                         Thread.CurrentThread.ManagedThreadId,

                                                                        DateTime.Now.ToLongTimeString());

                                _bacteriaCount++;  

                                Thread.Sleep(3000);  

                      }

    }

                      public Bacteria()

                      {

                           DoSomething();

                      }

             }

    O que fiz foi criar um membro estático do tipo SyncRoot, e no método DoSomething, adquirir um lock esclusivo nesse objeto. Assim, retiramos o lock da própria instância da classe do construtor da Bacteria. Como o membro _syncRoot é estático, ele estará visível para qualquer instância da classe Bacteria ou de suas classes filhas

  2. Fabio Galuppo says:

    Complementando a solução proposta. Outras 2 opções seriam:

    1. substituir o lock(this) por lock(typeof(Bacteria)). Assim as instâncias estariam apontando para o mesmo "sync root"

    2. eliminar o lock(this) e adornar o método DoSomething com o atributo System.Runtime.CompilerServices.MethodImpl( System.Runtime.CompilerServices.MethodImplOptions.Synchronized ). O resultado seria equivalente ao item 1.

    No entanto, por questões de recomendações e segurança, não é aconselhado fazer lock com elementos que são públicos. Ou seja, a idéia inicial é válida, mas criar uma classe para SyncRoot cria um certo overhead que poderia ser evitado. Devemos substituir a classe SyncRoot da solução proposta por um "private static object _syncRoot = new object();"

    Outra opção seria a aplicação de um "spin lock":

    private static int ProcessingThread = 0;

    private static void DoSomething()

    {

     while( 1 == Interlocked.CompareExchange( ref ProcessingThread, 1, 0 ) );

                               Interlocked.Exchange( ref ProcessingThread, 0 );

    }

    — Fabio Galuppo

  3. Roberto Farah says:

    Olá Fábio e Marcondes,

    Só posso dizer que o nível das respostas dos desafios está cada vez melhor!

    As soluções propostas estão muito bem elaboradas e gosto de ver quando mais de uma solução é apresentada, afinal, para a maioria dos problemas temos várias soluções com seus prós e contras, então as vezes é difícil dizer se uma determinada solução é mesmo a melhor. Logo mais publicarei minha resposta onde meu foco é em explicar, via depuração, o que ocorre com algumas soluções aparentemente corretas.

    Semana que vem vou publicar um artigo não técnico mas bem interessante… fiquem de olho!