Introduzione
Con il rilascio di .NET 11 Preview 1, Microsoft ha introdotto uno dei cambiamenti architetturali più significativi nella storia dell’async in .NET: il Runtime Async V2. Questo cambiamento sposta la responsabilità della gestione delle operazioni asincrone dal compilatore al runtime stesso, con impatti concreti su debug, profiling, leggibilità degli stack trace e potenzialmente sulle prestazioni.
In questo articolo analizziamo nel dettaglio come funziona il nuovo modello, come differisce dall’approccio attuale basato su state machine, come abilitarlo nei propri progetti e cosa aggiunge .NET 11 Preview 1 oltre al solo Runtime Async.
Il problema: le state machine del compilatore
Chiunque abbia lavorato seriamente con codice asincrono in C# conosce la frustrazione di leggere uno stack trace in produzione e trovarsi sommerso da frame generati dal compilatore. Ogni metodo async viene trasformato dal compilatore in una classe di stato (state machine) che implementa IAsyncStateMachine. Questa trasformazione è efficace, ma introduce livelli di indirezione che offuscano la reale catena di chiamate.
Un semplice stack di tre metodi async produce tipicamente oltre dieci frame nello stack trace live, la maggior parte appartenenti all’infrastruttura del compilatore (AsyncMethodBuilderCore.Start, ecc.). Il risultato è un debug più laborioso e strumenti di profiling che faticano a restituire una visione chiara dell’esecuzione.
Runtime Async V2: come funziona
Con Runtime Async, il compilatore non genera più la state machine. Emette invece un IL semplificato, annotato con [MethodImpl(MethodImplOptions.Async)], e delega al runtime la gestione della sospensione e ripresa dei metodi asincroni. In pratica, è il CLR stesso a tracciare l’esecuzione asincrona, non il codice generato dal compilatore.
Il risultato più visibile è nei live stack trace, ovvero ciò che profiler, debugger e new StackTrace() vedono durante l’esecuzione. Con Runtime Async, i metodi effettivi appaiono direttamente nello stack, senza wrapper di stato.
Confronto diretto degli stack trace
Consideriamo questo codice di esempio:
await OuterAsync();
static async Task OuterAsync()
{
await Task.CompletedTask;
await MiddleAsync();
}
static async Task MiddleAsync()
{
await Task.CompletedTask;
await InnerAsync();
}
static async Task InnerAsync()
{
await Task.CompletedTask;
Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}
Senza Runtime Async — 13 frame, con tutta l’infrastruttura del compilatore visibile:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__InnerAsync|0_2()
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__MiddleAsync|0_1()
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
...
(13 frame totali)
Con Runtime Async — 5 frame, la reale catena di chiamate:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
at Program.<Main>$(String[] args) in Program.cs:line 3
at Program.<Main>(String[] args)
È importante notare che questo miglioramento riguarda i live stack trace. Gli exception stack trace (catch (Exception ex)) già apparivano in modo pulito grazie all’ExceptionDispatchInfo nelle versioni precedenti.
Miglioramenti al debugging
Con Runtime Async, il debugger può finalmente fare ciò che ci si aspetterebbe da sempre:
- I breakpoint all’interno di metodi
asyncsi associano correttamente, senza essere deviati su codice generato - È possibile fare step-through attraverso i boundary degli
awaitsenza “saltare” nell’infrastruttura del compilatore - La finestra call stack del debugger mostra la catena reale, non i wrapper di stato
Questi miglioramenti avvantaggiano qualsiasi strumento che ispeziona lo stack live: profiler come dotTrace, logging diagnostico, e naturalmente il debugger integrato di Visual Studio e VS Code.
Come abilitare Runtime Async nel proprio progetto
Runtime Async è una feature in anteprima che richiede opt-in esplicito. Aggiungere le seguenti proprietà al file .csproj:
<PropertyGroup>
<Features>runtime-async=on</Features>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
Ovviamente, essendo ancora in anteprima, non è consigliato per ambienti di produzione. È tuttavia un ottimo momento per sperimentarlo su branch di sviluppo e fornire feedback al team .NET.
Requisiti hardware aggiornati
.NET 11 alza il baseline hardware richiesto. Per x86/x64, il minimo passa da x86-64-v1 a x86-64-v2, richiedendo istruzioni aggiuntive come SSE3, SSSE3, SSE4.1, SSE4.2 e POPCNT. Questo rientra nei requisiti già imposti da Windows 11 e copre tutta l’hardware Intel/AMD attualmente supportato ufficialmente (i chip più vecchi sono usciti dal supporto intorno al 2013).
Per Arm64 su Windows, il baseline aggiunge ora il requisito dell’instruction set LSE, richiesto da Windows 11 e da tutti gli Arm64 supportati da Windows 10.
Altre novità di .NET 11 Preview 1
Supporto nativo a Zstandard
Le librerie guadagnano il supporto nativo alla compressione Zstandard tramite la nuova classe ZstandardStream. Zstandard offre rapporti di compressione migliori rispetto a gzip con velocità di decompressione molto elevate — un’aggiunta benvenuta per pipeline di dati e API ad alta frequenza.
BFloat16 per AI e ML
Arriva il tipo BFloat16 (Brain Float 16), un formato floating-point a 16 bit nato per carichi di lavoro di machine learning. È ampiamente usato da librerie AI come TensorFlow e PyTorch, e la sua presenza nativa in .NET facilita l’integrazione con modelli ML senza conversioni intermedie.
Miglioramenti JIT
- Eliminazione dei bounds check: il JIT elimina ora i controlli ridondanti sul pattern
i + cns < len, comune nei loop su array e Span - Rimozione di contesti checked ridondanti: quando un valore è già noto essere nel range, i controlli di overflow vengono rimossi
- Devirtualizzazione in ReadyToRun: le immagini R2R possono ora devirtualizzare chiamate a virtual method generici non condivisi
Miglioramenti VM
Su piattaforme senza JIT (come iOS), l’interface dispatch ora usa un meccanismo di cache con miglioramenti di performance fino a 200x in codice ad alta intensità di interfacce. Guid.NewGuid() su Linux migliora del 12% circa usando la syscall getrandom() con batch caching.
C# 15: prime anticipazioni
.NET 11 Preview 1 include anche le prime feature di C# 15. Tra quelle già disponibili:
- Collection expression arguments: possibilità di specificare capacità, comparatori o altri parametri del costruttore direttamente nella sintassi delle espressioni di collezione
- Extended layout support: il compilatore emette
TypeAttributes.ExtendedLayoutper tipi annotati conExtendedLayoutAttribute, principalmente per scenari di interop
Conclusione
Il Runtime Async V2 rappresenta un passo importante verso un’esperienza di sviluppo asincrono più trasparente e debuggabile in .NET. Non è ancora pronto per la produzione, ma la direzione è chiara: Microsoft vuole che gli sviluppatori smettano di combattere con stack trace incomprensibili e possano finalmente fare debug dell’async come del codice sincrono.
Con .NET 11 previsto per novembre 2026 come Standard Term Support (STS), c’è ancora tempo per sperimentare e contribuire al processo di feedback prima del rilascio finale.
Fonte: What’s new in the .NET 11 runtime — Microsoft Learn | InfoQ: .NET 11 Preview 1