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!

Skip to main content