In questi giorni grazie ad un esigenza di Rob e Paolo, ho voluto approfondire un aspetto di cui ero già a conoscienza, ma del quale non avevo mai fatto test.
Come sapete, un campo dichiarato in una classe può teoricamente subire contemporaneamente l'accesso da più thread e spesso per rendere la classe thread-safe si ricorrono a tecniche di sincronizzazione per evitare che più thread cambino contemporanemante il campo. Non è detto infatti che una singola istruzione di assegnazione sia una singola istruzione di CPU e questo può portare ad inaspettati risultati. Questo problema si fa inoltre più frequente se si dichiara static (o shared in VB) il campo, condividendo il valore del membro per l'intero AppDomain.
A volte però, si vuole rendere l'istanza del campo accessible da chiunque purché dal medesimo thread, togliendo quindi il problema dell'accesso concorrente. L'attributo ThreadStatic permette di marcare un campo e di avere questo comportamento:
[ThreadStatic] private static object threadState;
Questa tecnica viene sfruttata per esempio dall'oggetto Transaction (System.Transactions) per mantenere l'oggetto rappresentante la transazione corrente cosicchè qualsiasi sotto chiamata possa accedere alla transazione corrente semplicemente chiamando Transaction.Current, il tutto isolato per thread (la transazione infatti non è visibile in altri thread, ma ne viene creata un'altra). Lo stesso comportamento, seppure meno comodo, si può ottenere usando Thread.AllocateNamedDataSlot, Thread.SetData e Thread.GetData che permettono di allocare per nome uno spazio, di impostarlo e di leggerlo, dove inserire un riferimento ad un oggetto.
La problematica di questo approccio è che nel caso in cui da un thread ne avviamo un altro e si vuole portare con se questi valori "speciali", occorre passarseli tramite argomenti o classi d'appoggio per poi essere copiati sul nuovo thread. Inoltre nel caso del ThreadPool, quell'insieme di thread già pronti all'uso, utilizzati quando si chiama la Begin*** di un delegate o si usa ThreadPool.QueueUserWorkItem, il campo statico risulta visibile alle operazioni che verranno prese in carico dal medesimo thread e mai rilasciato (si pensi al Dispose dell'oggetto) se non sovrascritto o impostato a null (il ThreadPool nel .NET 2.0 è composta da 25 Thread per CPU e di 500 IO Thread per CPU, in ASP.NET 2.0 sono 100/100, nel.NET 3.5 sono diventanti 250/500, mentre in ASP.NET 3.5 nella modalità AutoConfig sono rimasti invariati).
Per risolvere questo problema viene in aiuto la classe CallContext (System.Runtime.Remoting.Messaging) con i metodi LogicalGetData e LogicalSetData per leggere e impostare in base ad una chiave il riferimento ad un oggetto. Il comportamento è simile a ThreadStatic, ma questi valori sono relativi al contesto in cui sono stati impostati. Per capire meglio ho creato questa classe:
public class MyObject : Component { private readonly string _name; public MyObject(string name) { _name = name; } protected override void Dispose(bool disposing) { base.Dispose(disposing); Console.WriteLine("Disposed {0}", _name); } public override string ToString() { return _name; } }
E' una semplice classe che implementa IDisposable così da sapere quando l'oggetto, identificato con un nome, viene distrutto. Poi ho creato una ConsoleApplication che esegue su un thread del pool il controllo di un campo marcato con ThreadStatic, lo valorizza se è nullo e ne stampa il valore. La stessa operazione viene effettuata usando CallContext. Inoltre, attendo la fine dell'esecuzione per assicurarmi che le altre successive operazioni vengono effettuate nuovamente sul medesimo thread ed utilizzo GC.Collect per assicurarmi che gli oggetti, non più in uso, vengano distrutti (mi raccomando, in vere applicazioni non si deve chiamare se non in rare eccezioni):
class Program { [ThreadStatic] private static object threadState; static void Main(string[] args) { // Metodo da invocare su un Thread del pool WaitCallback wc = DoWork; for (int x = 0; x < 3; x++) { // Invoco e attendo che finisca wc.BeginInvoke(null, EndWork, wc).AsyncWaitHandle.WaitOne(); // Forzo la liberazione degli oggetti GC.Collect(); GC.WaitForPendingFinalizers(); } // Avvio l'operazione su un nuovo Thread Thread t = new Thread(DoWork); t.Start(); t.Join(); // Forzo la liberazione degli oggetti GC.Collect(); GC.WaitForPendingFinalizers(); Console.Read(); } private static void EndWork(IAsyncResult ar) { ((WaitCallback)ar.AsyncState).EndInvoke(ar); } const string DataKey = "test"; static void DoWork(object state) { Console.WriteLine("-----------------"); Console.WriteLine("Thread {0}", Thread.CurrentThread.ManagedThreadId); object t = CallContext.LogicalGetData(DataKey); if (t == null) { t = new MyObject("LogicalData"); Console.WriteLine("LogicalData set"); CallContext.LogicalSetData(DataKey, t); } else Console.WriteLine("LogicalData: {0}", t); t = threadState; if (t == null) { t = new MyObject("DataThread"); threadState = t; Console.WriteLine("DataThread set"); } else Console.WriteLine("DataThread: {0}", t); } }
Se lo si esegue, il risultato è il seguente:
----------------- Thread 6 LogicalData set DataThread set ----------------- Thread 6 LogicalData set DataThread: DataThread Disposed LogicalData ----------------- Thread 6 LogicalData set DataThread: DataThread Disposed LogicalData Disposed LogicalData ----------------- Thread 11 LogicalData set DataThread set Disposed DataThread
Come si vede, alla prima esecuzione di DoWork, entrambi i modi di memorizzare MyObject impostano il valore. Alla seconda esecuzione CallContext è di nuovo vuoto e va rivalorizzato, mentre threadState contiene il valore impostato sulla prima esecuzione. Lo stesso avviene anche alla terza esecuzione. E' interessante notare che l'oggetto impostato con CallContext viene effettivamente rilasciato e quindi ucciso dal GC solo all'avviarsi della nuova operazione sul medesimo thread (infatti il primo GC.Collect non ha sortito nessun effetto). Poiché i thread del pool non muoiono mai, DataThread non viene mai eliminato, ad eccezione di quello impostato nel Thread creato manualmente (l'esecuzione è finita e quindi il thread è inutilizzabile).
Tutto questo meccanismo è gestito dalla classe ExecutionContext (System.Threading) che è in grado di catturare, mettere a disposizione o di togliere il contesto e tutti valori ad esso associati. Quando si chiama ThreadPool.QueueUserWorkItem o il Begin*** di un metodo, viene chiamato ExecutionContext.Capture per catturare il contesto e tenerselo pronto quando il thread prende in consegna il delegate da eseguire, ripristinando quindi il contesto chiamando il metodo Run. Se avete mai dovuto creare un vostro ThreadPool, tipo questo, avrete sicuramente avuto a che fare con questa classe. Il bello di questo meccanismo è che se usiamo CallContext per mantenere dei valori ed invochiamo delegate asincroni o creiamo thread, automaticamente il contesto viene passato sull'altro thread condividendolo per la durata dell'operazione. Se nell'esempio precedente quindi si imposta prima di qualsiasi accodamento, l'istruzione CallContext.LogicalSetData(DataKey, new MyObject("test")); si ottiene a console:
Thread 6 LogicalData: test DataThread set ----------------- Thread 6 LogicalData: test DataThread: DataThread ----------------- Thread 6 LogicalData: test DataThread: DataThread ----------------- Thread 11 LogicalData: test DataThread set Disposed DataThread
Si noti che LogicalData su tutti i thread è già impostato.
Un'ultimo aspetto riguarda ASP.NET che utilizza anch'esso CallContext.HostContext ogni qualvolta si interroga HttpContext.Current, ma se si usano pagine asincrone con task ecc, i metodi asincroni non dispongono del contesto web e HttpContext.Current ritorna null. Diversamente usando CallContext.LogicalSetData si ha a disposizione l'oggetto anche sui task asincroni ed è quindi l'ideale se dovete mantenere DataContext (LINQ) o Session (NHibernate).
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
- LINQ, lazy loading e architettura, l'11 marzo 2011 alle 18:42
- MetadataDiffViewer: aggiornato al .NET Framework 4.0, Silverlight 4.0 e Sharepoint 2010, il 7 gennaio 2010 alle 13:58
- .NET Framework 4.0 beta 1: Windows Communication Foundation, il 18 maggio 2009 alle 16:00
- Parallelizzare in Silverlight 2.0, il 21 aprile 2009 alle 00:25
- Silverlight: performance dell'isolated storage, il 16 aprile 2009 alle 17:38
- MetadataDiffViewer: differenze tra i framework, il 15 aprile 2009 alle 18:56