Al rientro dalla ferie mi sono trovato a gestire un problema particolarmente rognoso che riguardava tra le altre cose l'utilizzo imporprio della classe Lazy.
Siccome in realtà non conoscevo in modo approfondito questa classe, ho dovuto studiarla e ho verificato come in realtà questa possa essere utile in diversi contesti.
Ho anche ritenuto l’argomento abbastanza interessante e per questo ho pensato di scriverne qualcosa.Spero possa interessare anche Voi.
Iniziamo con il dire a cosa serve: lo scopo del Lazy è quello di gestire in modo efficace un tipo di variabile T molto “pesante” e impegnativa, generalmente rappresentato da una classe (anche se può essere un qualsiasi tipo reference o value).
Con il termine pesante mi riferisco a una classe che gestisce al suo interno molti dati e/o la cui procedura di attivazione è molto onerosa.
In pratica Lazy permette di ottenere l’inizializzazione differita: quindi il tipo T specificato non sarà mai instanziato, e quindi occuperà risorse, sino a quando non verrà utilizzato almeno una volta.
L’utilizzo pratico è relativamente semplice: nella dichiarazione dell'oggetto Lazy si sostituisce a T il nome della classe, e quindi quando occorre un'istanza della classe T si utilizza la proprietà value offerta da Lazy.
Certo, è possibile assolutamente scrivere per nostro conto la logica che permetta di instanziare un oggetto solo quando e se necessario, ma il .Net ci permette di avere questa possibilità che in realtà ci esemplifica non poco il lavoro.
La classe la Lazy dispone di due proprietà.
IsValueCreated è booleano e indica se l’istanza dell’oggetto T è stata già stat instanziato o meno: Value ne restituisce l’istanza.
Se l’oggetto T non è stato instanziato questo viene automaticamente creato la prima volta che si usa la proprietà Value.
Come sempre un esempio dice più di mille parole.
class Program { static void Main(string[] args) { Lazy mClasseTest = new Lazy(); Console.WriteLine("ClasseTest - Valore IsValueCreated = " + mClasseTest.IsValueCreated.ToString()); Console.WriteLine("Valore stringaTest=" + mClasseTest.Value.stringaTest); Console.WriteLine("ClasseTest - Valore IsValueCreated = " + mClasseTest.IsValueCreated.ToString()); Console.Read(); } } public class ClasseTest { public string stringaTest { get; set; }
public ClasseTest() { Console.WriteLine("ClasseTest: Costruttore richiamato"); stringaTest = "XX"; }
}
Ecco quanto si ottiene
ClasseTest - Valore IsValueCreated = False ClasseTest: Costruttore richiamato Valore stringaTest=XX ClasseTest - Valore IsValueCreated = True
Direi che quanto sopra è abbastanza esplicativo: non è bastato dichiarare l’oggetto con new Lazy() per creare l'oggetto.
Iinfatti il costruttore della classe ClasseTest con questa istruzione non viene assolutamente richiamato e quindi non vengono impegnate le relative risorse.
Solo affrontando successivamente la proprietà Value si ottiene la costruzione dell’oggetto, con relativo impegno delle risorse.
A questo punto sorge spontanea (?) una domanda: e se la classe T ha un costruttore che accetta parametri ???
Allora non si può usare quanto sopra, che lavora solo con classi di tipo T che dispongono di costruttore senza parametri.
Ci viene però in aiuto una altro costruttore disponibile per Lazy che accetta come parametro un Func<T> e che serve proprio a questo scopo: instanziare nel modo corretto il tipo T contenuto in Lazy.
class Program { static void Main(string[] args) { Lazy mClasseTest = new Lazy(InitClasseTest); Console.WriteLine("ClasseTest - Valore IsValueCreated = " + mClasseTest.IsValueCreated.ToString()); Console.WriteLine("Valore stringaTest=" + mClasseTest.Value.stringaTest); Console.WriteLine("ClasseTest - Valore IsValueCreated = " + mClasseTest.IsValueCreated.ToString()); Console.Read(); } static ClasseTest InitClasseTest() { return new ClasseTest("YY"); } } public class ClasseTest { public string stringaTest { get; set; } public ClasseTest(string mStr) { Console.WriteLine("ClasseTest: Costruttore richiamato"); stringaTest = mStr; } }
Come esposto sopra all’interno della Func<T> è possibile inserire qualsiasi logica legata alla creazione della classe.
Ora una precisazione pedante ma doverosa per evitare confusione: se si usa il primo costrutto visto, cioè nessun argomento passato alla classe Lazy ad eccezione del tipo della classe, quando si accede alla proprietà value la prima volta verrà automaticamente richiamato il costruttore della classe T.
Lazy<T>()
Se invece si usa il secondo metodo, ove si passa a Lazy una Func, questa dovrà restituire a sua volta un’istanza della classe T.
Lazy<T>(Func<T>)
Quindi questa volta all’accesso della proprietà value della classe Lazy verrà richiamato solo la Func, che essendo delegata a restituire a sua volta un’istanza della classe T, giocoforza ne richiamerà anche il costruttore.
Nel contesto di utilizzo del Lazy si usa il termine di inizializzatore della classe T per identificare il costruttore di questa oppure la Func<T>.
Quanto esposto sopra non ha bisogno di ulteriori delucidazioni: purtroppo le cose si complicano quando mettiamo a mezzo i thread.
Infatti a fronte di più thread concorrenti come si comporta il Lazy ?? Si rende necessario fare alcune precisazioni.
Oss.: Anche se il nostro programma non usa esplicitamente i thread ricordo sommessamente che usare async/await in pratica crea nuovi thread. Per tale motivo è molto più facile di quello che sembra avere a che fare con thread concorrenti e anche per tale motivo ritengo utile esplicitare il comportamento della classe Lazy in ambito multithread.
Per iniziare occorre dire che tutti i thread riceveranno da Lazy sempre la stessa istanza della classe T. Questa affermazione è banale e scontata, ma vedremo che potrebbe non essere così ovvia.
Un’altra parte importante che occorre evidenziare è che by default (cioè senza fornire alcun parametro speciale al costruttore del Lazy oltre eventualmente alla Func) gli oggetti Lazy sono assolutamente thread-safe.
Cooooooosa devo sentire: qualcuno di Voi non sa precisamente cosa vuol dire thread-safe ?? Siete certi ??
Vabbè: un piccolo ripassino.
La caratteristica principale di avere più thread è che questi vedono e sono in grado di modificare le stesse variabili.
Il problema che può occorrere è che se più di un thread aggiornano la stessa variabile contemporaneamente allora per diversi motivi questa al termine delle modifiche può avere un valore inconsistente o errato.
Per tale motivo è necessario che ogni variabile sia aggiornata sicuramente da solo un singolo thread alla volta.
Altro problema che può nascere è relativo al fatto che più thread tentino di accedere a risorse che per loro natura accettano un solo accesso alla volta (per esempio un file in modalità scrittura o anche l’accesso a una porta USB).
I punti del codice delegate a eseguire operazioni che posso provocare problematiche se eseguite da più thread contemporaneamente viene denotato con il termine sezione critica.
Quindi con il termine thread-safe si indica la caratteristica di aver messo in sicurezza tutte le sezioni critiche: esistono, infatti, tutta una serie di strumenti che permettono a queste parti di essere eseguite nel modo corretto e quindi da solo un thread alla volta facendo eventualmente attendere gli altri thread concorrenti.
Inutile dire che solo il codice thread-safe è l’unico che deve essere utilizzato quando il software utilizza più thread, pena il verificarsi di strani problemi molti dei quali quasi impossibili da identificare in modo preventivo.
Ritorniamo alla domanda iniziale: cosa significa nel dettaglio che Lazy è thread-safe ??
Vuole dire in pratica che l’inizializzatore della classe T (costruttore di T o Func) viene richiamato in modo thread-safe, così come l’accesso alle proprietà Value e IsValueCreated.
Tutto questo è messo a disposizione dal linguaggio senza che noi dobbiamo fare alcunchè.
Ovviamente una volta ottenuto l’oggetto di tipo T da Lazy impegnado la proprietà Value, l’accesso alle proprietà e metodi di quest’ultimo rimane come era prima.
E’ la classe Lazy ad essere thread-safe, non T !
Scusate se insisto su questo punto, che è basilare e foriero di problemi.
Se si lavora in ambienti concorrenti multithread una volta ottenuto l’oggetto T da Lazy, i vari metodi e proprietà della classe T devono essere implementati in thread-safe: se non lo sono il semplice fatto di essere memorizzato in un oggetto di Lazy non li rende tali.
Lazy<T> non fa miracoli: non è in grado di trasformare una classe NON thread-safe in una thread-safe e viceversa !
….la thread-safitudine vale solo per le due proprietà dell’oggetto Lazy e per l’inizializzatore della classe.
L’oggetto T ivi contenuto rimane quello che è…..
Spero questo argomento Vi abbia interessato, e Vi rimando alla prossima parte della serie.
Linkografia
Laziness in C# 4.0 – Lazy
Attenzione: quest'ulltimo link è relativo a un post che riporta informazioni probabilmente inerenti a versioni in preview di .Net 4.0. Per tali motivi il nome dei paramteri è stato variato: i concetti esposti, comunque, rimangono validi.