Molti di voi avranno già avuto modo di sperimentare con la nuova release di .NET Framework 3.5 beta (codename "Orcas"). Questo articolo vuole essere una guida alle novità del prodotto e una spiegazione non solo delle nuove funzionalità, ma anche dei passi che hanno portato alla progettazione finale e i motivi per quali le funzionalità stesse sono state create.
La struttura di .NET Framework 3.5 "Orcas"
Questa nuova release segue il principio introdotto con .NET Framework 3.0 di rilasciare un aggiornamento incrementale delle librerie senza portare alcun cambiamento visibile al runtime. Ciò garantisce un altissimo standard di compatibilità con applicazioni scritte per .NET Framework 2.0 e 3.0, consentendo di aggiungere funzionalità opzionali che sfruttano le novità presenti in Orcas. Per raggiungere questo obiettivo, Orcas è stato progettato in due parti (chiamate convenzionalmente Red bits e Green bits): i Red Bits sono di fatto un Service Pack per .NET 2.0 e 3.0, mentre i Green Bits sono assembly completamente nuovi che introducono funzionalità aggiuntive nella piattaforma.
Grazie a questa architettura, un componente realizzato con .NET 3.0, girerà senza problemi con .NET 3.5 e viceversa, un componente che dipende solo dai Red Bits di Orcas girerà senza problemi su .NET 3.0 (e .NET 2.0 se non avete usato WF, WCF o WPF).
Maggiori informazioni su questa architettura sono disponibili sui blog degli executive di .NET e di Visual Studio (Jason Zander e S. Somasegar).
Il CLR in .NET Framework 3.5 "Orcas"
Alla luce di quanto detto sopra a proposito dell'architettura fondamentale della release, si può asserire che il CLR non è cambiato molto in superficie. Le novità ci sono, ma sono sotto il cofano senza essere per questo meno importanti. In particolare, le principali innovazioni nel runtime sono:
- miglioramenti nelle prestazioni a 64 bit, in particolare per quanto riguarda il Working Set e la velocità di compilazione al volo;
- la reingegnerizzazione del thread-pool, che consente una migliore scalabilità e migliori prestazioni;
- nuove modalità di funzionamento del Garbage Collector.
Tutte queste funzionalità sono disponibili anche per le applicazioni scritte usando .NET 2.0 e 3.0 ed eseguite su una macchina con .NET 3.5.
I miglioramenti nelle prestazioni con SO a 64 bit
I dettagli delle modifiche introdotte per migliorare le performance sono alquanto esoterici e richiedono conoscenze dell'implementazione del runtime non troppo interessanti per gli sviluppatori di applicazioni: quello che è interessante sapere è che l'obiettivo è stato quello di ridurre il working set per applicazioni che girano in un OS a 64 bit. Il problema è stato affrontato migliorando il layout delle pagine di codice su disco, in modo da avere la logica utilizzata più frequentemente e inizialmente dalle applicazioni tipiche memorizzata in blocco, in modo da caricare un numero inferiore di pagine in memoria.
Abbiamo anche ridotto in parte l'overhead dei system assembly a 64 bit in modo tale da contenere le allocazioni di memoria fatte dagli assembly stessi. Questo è più un miglioramento strutturale che non si riflette direttamente in una riduzione del working set delle applicazioni, ma riduce l'overhead associato con il runtime stesso.
Per quanto riguarda i miglioramenti del compilatore JIT, abbiamo ridotto notevolmente i tempi di compilazione in alcuni casi particolari che però non sono comuni alla maggior parte delle applicazioni che girano sui framework a 64 bit.
Il nuovo Threadpool
Con chip multi-core sempre più comuni sia sui client sia sui server, un numero crescente di applicazioni tende ad adottare pattern asincroni. Ciò include tecniche che generano un numero arbitrario di task in parallelo, oltre ad eseguire operazioni asincrone di I/O. Il Threadpool del CLR è la funzionalità progettata per soddisfare queste esigenze: incapsula algoritmi sofisticati e astrae i concetti di task scheduling e di integrazione delle I/O completion port.
Nel CLR incluso in .NET fino alla versione 3.0, il throughput per assegnare worker item e richieste di I/O completion non è ottimizzato, soprattutto quando lo si confronta con il nuovo threadpool in Windows Vista. Questo problema ha fatto sì che in passato molti potenziali utenti del threadpool hanno finito per scriversi il proprio, finendo per reinventare la ruota, cadendo spesso in tranelli e bug noti e soprattutto finendo tagliati fuori dal beneficiare delle innovazioni future nell'implementazione fornita dal CLR.
In .NET 3.5 il problema è stato affrontato e risolto.
Nelle versioni di .NET sino alla 3.0, il Threadpool è incluso per la maggior parte nel codice dell'execution engine, che a sua volta è codice nativo. I principali componenti sono:
1) la coda dei work item da eseguire;
2) l'insieme dei worker thread utilizzabili per soddisfare le richieste accodate in 1);
3) un Windows I/O Completion Port globale, che è concettualmente una coda di routine di I/O completion da eseguire gestita da Windows. Queste routine includono le strutture OVERLAPPED che puntano al delegate del CLR da eseguire;
4) l'insieme dei thread di I/O utilizzabili per soddisfare le richieste accodate in 3).
I task sono inseriti in coda tramite le API pubbliche e l'infrastruttura del Framework, e le routine di thread start associate assicurano che un thread preso da 2) o 4) processi questi task appena possibile. Il numero di thread disponibili è variabile, dal momento che il Threadpool può decider di iniettare nuovi thread o pensionare thread esistenti, basandosi su un numero di parametri che consentono di bilanciare dinamicamente il carico di lavoro. In pratica, quando il Threadpool vede le code allungarsi e molti thread bloccati, decide di aggiungere worker threads, mentre, se le code sono vuote, può decidere di eliminarne.
In .NET fino alla versione 3.0, sia i worker thread sia i thread di I/O completion spendono molto tempo nell'execution engine (EE), perché qui è dove sono situate le strutture dati di cui hanno bisogno e la logica di dispatching. In altre parole, quando un work item è schedulato tramite una chiamata a ThreadPool.QueueUserWorkItem, un puntatore al delegate e le informazioni associate sono memorizzati in una code interna nell'EE. Se un worker thread non trova task in coda, si sospende e aspetta che un evento sia segnalato; la prossima chiamata a ThreadPool.QueueUserWorkItem segnala l'evento e sveglia un worker thread per processare il task. Ovviamente, se il worker thread trova work items in coda non si sospende, ma il check deve essere fatto nell'EE. La struttura è la stessa per i thread di I/O.
Quando un worker thread processa un task, l'operazione principale è l'invocazione di un delegate. Ma poiché questa invocazione in .NET 2.0 è originata all'interno dell'EE, questo richiede che tutta l'infrastruttura di una transizione da managed code a native code sia messa in piedi, inclusi i frame per la gestione delle eccezioni, la transizione all'interno dell'Application Domain in cui il worker è stato creato e la ricostruzione dell'execution context se ne è stato catturato uno. Questa transizione è stata individuata come la componente principale nelle performance misurate, sia per worker thread sia per thread di I/O.
Nel CLR di .NET 3.5, pertanto, abbiamo introdotto il seguente cambiamento: sia i worker thread sia i thread di I/O ora ricevono e distribuiscono i task rimanendo sempre in managed code, eliminando tutte le transizioni nel codice nativo dell'EE. Se un thread si sospende, salvo rare eccezioni questo succede in managed code. Un thread non deve attraversare il confine managed/native solo per verificare se c'è lavoro da fare.
L'innovazione è illustrata da questo diagramma:
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.