In questa terzaparte si continua ad esporre l’utilizzo del Lazy in ambiti multithread.
Quanto visto nella parte precedente cioè l'utilizzo della classe Lazy senza usare costrutti particolariespone come anche a fronte di più thread tutto funziona correttamente e senza intoppi.
Possono però esistere casi reali in cui si rende necessario avere maggiore possibilità di configurarne il comportamento.
Per esempio l’inizializzatore della classe T, o anche il codice a contorno, può avere al suo interno dei lock o altro, che possono non andare d’accordo con i lock presenti all’interno della classe Lazy, e questa coesistenza può provocare deadlock.
In altre parole come detto l’oggetto Lazy è thread-safe, e quindi per esempio affinché il codice dell’inizializzatore della classe T sia tale vengono usati giocoforza all’interno del framework qualche sorta di lock: questo non solo introduce penalità di perfomance ma può portare (anche se in rari casi) a deadlock che possono occorrere sopratutto a fronte della presenza di altri lock posti nel codice utilizzatore scritto da noi.
Un altro caso può essere legato al fatto che pur essendo in ambiente multithread non siamo interessati al fatto che la classe Lazy sia thread-safe poiché si desidera gestire questo aspetto nel codice consumatore.
In altri termini si desidera delegare la gestione dell’accesso concorrente all’inizializzatore nonché alle proprietà Value e IsValueCreated a codice scritto esternamente da noi, questo con l’ovvio intento di spremere al massimo le perfomance.
Per risolvere questi due problemi è possibile passare come argomento di Lazy un enumeratore LazyThreadSafetyMode.
Lazy(Func, LazyThreadSafetyMode)
Lazy(LazyThreadSafetyMode)
Questo propone le seguenti possibilità.
None
ExecutionAndPublication
PublicationOnly
ExecutionAndPublication è il default e il comportamentp è quello visto nel posto precedente.
None permette di risolvere il secondo punto visto sopra: nessun thread-safe.
L’accesso ai metodi della classe Lazy deve essere gestito da codice esterno consumatore pena problemi vari legati al fatto che il codice è non thread-safe. Potete provare voi stessi usando il seguente codice.
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,LazyThreadSafetyMode.None); 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); } }
Sicuramente dopo qualche tentativo il codice emetterà qualche forma di errore: questo è dovuto al fatto che abbiamo in pratica levato la caratteristica thread-safe di Lazy, e si tenta di usarlo proprio con accessi da parte di più thread contemporaneamente.
Un discorso a parte merita PublicationOnly, che permette di risolvere il primo punto delle richieste esposte sopra.
In questo caso ogni thread che richiamerà Value impegnerà l’inizializzatore della classe, e quindi verrà creata un’istanza per ogni thread che ne fa richiesta: solo che verrà mantenuta solo la prima istanza creata e le altre saranno immediatamente eliminate.
Chiariamo meglio con il codice: sostituiamo al codice sopra il costruttore di Lazt<T> come nel seguito.
lazyClasseTest = new Lazy(InitClasseTest,LazyThreadSafetyMode.PublicationOnly);
In tal caso il risultato sarà una cosa del genere.
Accesso a ThreadProc da parte del thread 3 Accesso a ThreadProc da parte del thread 4 Accesso a ThreadProc da parte del thread 5 Accesso a InitClasseTest da parte del thread 3 Accesso a InitClasseTest da parte del thread 5 Costruttore di ClasseTest richiamato dal thread id 3. Accesso a InitClasseTest da parte del thread 4 Costruttore di ClasseTest richiamato dal thread id 4. Costruttore di ClasseTest richiamato dal thread id 5. Classe inzializzata dal thread 3; thread corrente: 3. Classe inzializzata dal thread 3; thread corrente: 5. Classe inzializzata dal thread 3; thread corrente: 4.
Il risultato può differire a seconda del thread che il sistema ha favorito, ma il concetto che si vuole evidenziare ritengo sia ben chiaro.
Arriva il primo thread (il numero 3) che instanzia la classe tramite l’ausilio della Func InitClasseTest.
Quindi ottiene in ThreadProc questa stessa istanza. Giunge poco dopo (o anche contemporaneamente) un altro thread, nel nostro caso il numero 4, che a sua volta creerà un’altra istanza della classe: solo che questa istanza, dopo essere stata creata, viene distrutta.
Infatti in ThreadProc il thread 4 ottiene un’istanza della stessa classe ottenuta dal thread 3.
Analoga cosa accade per il thread 5.
Quindi in definitiva possiamo affermare che anche in questo caso Lazy assicura che tutti i thtread ottengano la stessa istanza della classe T anche a fronte di accessi concorrenti.
Oss. Nel caso in cui si voglia dotare ogni thread di un’istanza della classe T differente occorre rivolgersi, ad esempio, alla classe ThreadLocal.
Inutile dire che questo processo di instanza di classi che poi vengono distrutte e non riutilizzate introdice penalità di perfomance significative, ma possono esserci casi in cui questo risulta essere il male minore.
Ma quali possono essere gli usi pratici per la Lazy ?
Devo ammettere che la prima volta che ho affrontato questa classe mi è subito venuta in mente che proponesse la classica “soluzione alla caccia di un problema”.
Ma non è così.
Si immagini di avere una classe che rappresenta una fattura: questa per sua natura è composta da dati di testa (data, cliente, etc) e da una o più righe.
Se si ponesse l’intera logica di rappresentazione della fattura in una classe si avrebbe che al richiamo, per esempio, di un documento memorizzato su database l’oggetto sarebbe popolato di tutti i dati costitutivi (dati di testa + tutte le righe).
Nella pratica possono esistere numerosi casi in cui si possa avere necessità dei soli dati di testa del documento: pertanto la logica che sottende alla popolazione dei dati delle righe sarebbe in casi come questi inutile e anzi impegnerebbe risorse per dati inutilizzati.
Ecco che viene in aiuto il Lazy: le righe potrebbe essere proposte all’esterno della classe una proprietà che espone le righe proprio con l'ausilio di Lazy.
Lazt<List<RigheFattura>>
In tal modo le righe sarebbero caricate solo al loro reale utilizzo.
Certo, potrebbe essere aggiunta una logica nella classe che esegue la stessa cosa, ma la soluzione proposta è quella che porta al risultato più semplice e più elegante.
D’altronde l’esempio proposto è analogo a quanto fa Entity Framework per gestire soluzioni simili.
Un altro caso di utilizzo è relativo all’implementazione di classi singleton.
Infatti i sacri testi dicono che per implementare una classe singleton occorre fare qualcosa del genere.
public class SingletonClass { private static readonly SingletonClass istanzaSingletonClass = null; private SingletonClass() { ... } public static Singleton Instance { get { if (istanzaSingletonClass == null) istanzaSingletonClass = new SingletonClass(); return istanzaSingletonClass; } } }
Quanto sopra è vero, funziona, ma ha un grosso difetto: non è thread-safe.
Invece che trafficare con lock e similari è possibile renderla thread-safe nel modo seguente.
public class Singleton { private static readonly Lazy instanceHolder = new Lazy(() => new Singleton()); private Singleton() { ... } public static Singleton Instance { get { return instanceHolder.Value; } } }
Più semplie ed elegante e sopratutto..... thread-safe.
In generale il mio consiglio è quello sempre di valutare l’utilizzo della classe Lazy<T> tutte le volte che ci si trova a fare qualcosa del genere.
if (classeenorme == null) foo = new classeenorme();
Spero di avervi se non convinto almeno mostrato una classe interessante e notevole a disposziione di C# e del .Net framework
Linkografia
C#: System.Lazy and the Singleton Design Pattern
.