Ecco la seconda parte della piccola serie dedicata all'oggetto Lazy<T>. Nel. post precedente ho inziato a esporre i rudimenti iniziali relativi alla classe.

Ora occorre anche ragionare su cosa succede in ambienti multi-thread quando si richiama la proprietà value da Lazy.

 

Come ci si aspetta il primo thread che impegna la proprietà value instanzia la classe T, e la stessa istanza viene quindi distribuita a tutti i thread che ne fanno successivamente richiesta. L’esempio nel seguito, scopiazzato senza tanti riguardi dall’msdn di M$ (in linkografia) mostra tutto questo.

 class Program
{
  static Lazy lazyClasseTest = null;

  static ClasseTest InitClasseTest()
  {
    Console.WriteLine("Accesso a InitClasseTest da parte del thread " + Thread.CurrentThread.ManagedThreadId);
    ClasseTest mClasseTest = new ClasseTest(Thread.CurrentThread.ManagedThreadId);

    return mClasseTest;
   }

  static void Main(string[] args)
  {
    lazyClasseTest = new Lazy(InitClasseTest);

    Thread[] threads = new Thread[3];
    for (int i = 0; i < 3; i++)
    {
      threads[i] = new Thread(ThreadProc);
      threads[i].Start();
    }

    foreach (Thread t in threads)
    {
      t.Join();
    }
    Console.ReadLine();
  }

  static void ThreadProc()
  {
    Console.WriteLine("Accesso a ThreadProc da parte del thread " + Thread.CurrentThread.ManagedThreadId);
    ClasseTest istanza_classetest= lazyClasseTest.Value;
    try
    {
      Monitor.Enter(large);
      istanza_classetest.IdThreadAttuale = Thread.CurrentThread.ManagedThreadId;
      Console.WriteLine("Classe inzializzata dal thread {0}; thread corrente: {1}.",
      istanza_classetest.IdThreadInzializzante.ToString(), istanza_classetest.IdThreadAttuale.ToString());
    }
    catch (Exception e)
    {
      throw;
    }
    finally
    {
      Monitor.Exit(large);
    }
  }
}

class ClasseTest
{
  public int IdThreadInzializzante { get { return _IdThreadInzializzante; } }
  private int _IdThreadInzializzante = 0;

  public int IdThreadAttuale { get; set; }

  public ClasseTest(int _IdThread)
  {
    _IdThreadInzializzante = _IdThread;
    Console.WriteLine("Costruttore di ClasseTest richiamato dal thread id {0}.", _IdThreadInzializzante);
  }
}

Risultato ottenuto

Accesso a ThreadProc da parte del thread 3
Accesso a ThreadProc da parte del thread 4
Accesso a InitClasseTest da parte del thread 3
Accesso a ThreadProc da parte del thread 5
Costruttore di ClasseTest richiamato dal thread id 3.
Classe inzializzata dal thread 3; thread corrente: 3.
Classe inzializzata dal thread 3; thread corrente: 4.
Classe inzializzata dal thread 3; thread corrente: 5.

Da osservare in ThreadProc la struttura con Monitor.Enter e Monitor.Exit: questa è in tutto e per tutto analoga a usare lock per marcare una sezione critica.

Però esistono alcuni vantaggi nell’usare una struttura di questo genere, per esempio una maggiore possibilità di configurazione che può essere utile in alcuni casi: per tale motivo ogni tanto la preferisco.

Qui il problema da risolvere è che l’accesso alla proprietà IdThreadAttuale della classe non è thread-safe, e quindi deve essere rinchiusa in una sezione critica ad accesso esclusivo da parte di un solo thread alla volta. I thread vengono creati in un modo becero ma efficace e tramite Join si attende affinchè tutti siano terminati al fine di raccogliere l’esito finale di esecuzione

Il risultato presentato può variare leggermente a seconda di quale thread il nostro sistema favorisca, ma in buona sostanza il concetto che voglio esporre è che solo il primo thread (nel nostro caso il numero 3 perchè è il primo ad essere arrivato, ma potrebbe essere un altro) richiama il costruttore: tutti gli altri ottengono la stessa istanza della classe.

Anche l’inizializzatore della classe, nel nostro caso InitClasseTest, viene richiamato solo la prima volta dal primo thread. Tutte le altre volte nulla viene richiamato.

A quanto sembra tutto funziona alla grande, o almeno si spera, ma occorre valutare alcune cose, tenendo ben presente che la classe T se usata in questo modo (cioè tramite Lazy<T>) sicuramente nella realtà impegna parecchie risorse, e quindi ricercare il massimo delle perfomance può essere doveroso.

Ma per questo Vi rimando alla terza e ultima parte della serie.

Linkografia

Lazy initialization