Derniers tips / Mémos
Modélise nativement « exactement l'un de ces types », avec un switch exhaustif vérifié par le compilateur.
Fin des state machines générées par le compilateur : moins d'allocations et des stack traces enfin lisibles.
Recherche sémantique directement dans SQL Server 2025, sans base vectorielle séparée, en LINQ typé.
Framework UI cross-platform pour .NET : Windows, macOS, Linux, iOS, Android et WebAssembly.
Découvrez les nouvelles APIs asynchrones pour la compression ZIP et les améliorations de performance dans .NET 10
.NET 11 : runtime async, la fin des state machines générées
Depuis C# 5, chaque méthode async est réécrite par le compilateur en une machine à états : une classe imbriquée, un champ d'état, une méthode MoveNext. Ce mécanisme fonctionne, mais il coûte des allocations et pollue toutes tes stack traces. .NET 11 déplace ce travail dans le runtime : le compilateur émet un simple appel, et c'est la VM qui gère la suspension. Ton code source, lui, ne change pas d'une ligne.
Sommaire
- Ce que génère le compilateur aujourd'hui
- Ce que change runtime async
- Activer et désactiver
- Des stack traces lisibles
- Covariance Task vers Task<T>
- Cas d'utilisation concrets
- Avantages et inconvénients face au modèle async classique
Ce que génère le compilateur aujourd'hui
Écris une méthode async de trois lignes et le compilateur produit une classe entière. Elle implémente IAsyncStateMachine, stocke l'état d'avancement dans un champ entier, et concentre tout le corps de ta méthode dans un MoveNext organisé en table de saut.
// What you write
public async Task<int> LoadAsync(HttpClient client)
{
string body = await client.GetStringAsync("https://example.com");
return body.Length;
}
// What the compiler emits today, simplified
[CompilerGenerated]
private sealed class LoadAsyncStateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder<int> Builder;
public HttpClient Client;
private TaskAwaiter<string> awaiter;
// The whole method body becomes a jump table over State
public void MoveNext() { /* ... */ }
public void SetStateMachine(IAsyncStateMachine stateMachine) { /* ... */ }
}Ce que ça coûte
- Une allocation sur le tas dès que la méthode suspend réellement (boxing de la structure d'état)
- Des frames MoveNext parasites dans chaque pile d'appels
- Un IL volumineux, plus difficile à inliner et à optimiser
- Des noms générés illisibles dans les profileurs et les dumps mémoire
Ce que change runtime async
Avec runtime async, le compilateur n'émet plus de classe d'état. Il émet un appel direct vers le runtime, qui prend en charge la suspension et la reprise. L'IL généré tient en quelques instructions, et le JIT peut l'optimiser comme n'importe quel appel.
// Exact same source, compiled with runtime-async=on
public async Task<int> LoadAsync(HttpClient client)
{
string body = await client.GetStringAsync("https://example.com");
return body.Length;
}
// Emitted IL, simplified: no state class, a direct runtime call
// call !!0 System.Runtime.CompilerServices.AsyncHelpers::Await<string>(
// class System.Threading.Tasks.Task<!!0>)Ce que tu y gagnes
- Moins d'allocations : la structure d'état n'est plus systématiquement promue sur le tas
- Un IL nettement plus simple, donc plus de marge pour l'inlining
- Depuis la Preview 4, l'inlining fonctionne aussi à la compilation ReadyToRun via crossgen2
- Les bibliothèques du runtime .NET 11 sont désormais compilées avec runtime-async=on
Activer et désactiver
Les bibliothèques du runtime en bénéficient déjà. Pour tes propres assemblys, l'activation passe par une propriété MSBuild. Attention : la fonctionnalité a évolué à chaque preview, vérifie la note de version de la preview que tu utilises.
Activer sur ton projet
Cible net11.0 et ajoute la feature au compilateur. En .NET 11 Preview 3, l'attribut RequiresPreviewFeatures n'est plus nécessaire.
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<!-- Opt in for your own assemblies -->
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>Sortie de secours
Si un profileur, un réécrivain d'IL ou un outil de couverture n'est pas encore compatible, tu peux désactiver la fonctionnalité au niveau du projet. À noter : le commutateur d'environnement DOTNET_RuntimeAsync a été supprimé, il n'y a plus de désactivation à chaud.
<PropertyGroup>
<!-- Escape hatch when a profiler or IL rewriter is not ready yet -->
<UseRuntimeAsync>false</UseRuntimeAsync>
</PropertyGroup>Des stack traces lisibles
C'est le bénéfice le plus immédiat, et celui qui se voit dès le premier incident en production. Les frames MoveNext synthétiques disparaissent : la pile d'appels ressemble enfin aux méthodes que tu as écrites.
# Before: MoveNext frames and dispatch noise
at Shop.Orders.OrderService+<LoadAsync>d__4.MoveNext()
--- End of stack trace from previous location ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Shop.Api.OrdersController+<Get>d__2.MoveNext()
# After: the frames match the methods you wrote
at Shop.Orders.OrderService.LoadAsync(HttpClient client)
at Shop.Api.OrdersController.Get(Int32 id)Concrètement, cela change aussi la lecture des logs et des outils d'APM : le regroupement d'erreurs devient plus fiable, puisqu'il ne s'appuie plus sur des noms de types générés.
Covariance Task vers Task<T>
Autre apport lié de .NET 11 : une méthode dérivée peut désormais retourner Task<T> là où la méthode de base retourne Task. Le runtime génère l'adaptateur nécessaire pour que la répartition virtuelle reste correcte. Cela supprime une bonne partie des surcharges d'adaptation écrites à la main.
public abstract class Repository
{
public abstract Task SaveAsync();
}
// .NET 11 accepts an override returning Task<T> where the base returns Task
public sealed class SqlRepository : Repository
{
public override Task<int> SaveAsync() => Task.FromResult(1);
}Cas d'utilisation concrets
Runtime async ne demande aucune réécriture. Ce sont surtout les charges fortement asynchrones qui en profitent.
- API ASP.NET Core à fort débit : chaque requête traverse des dizaines de méthodes async, l'allocation économisée se cumule
- Workers et consommateurs de files : boucles de traitement longues où la pression sur le GC est le facteur limitant
- Bibliothèques réutilisées largement : recompiler ta librairie avec runtime-async profite à tous ses appelants
- Diagnostic d'incidents : piles d'appels exploitables sans décodage mental des noms générés
- Applications sensibles à la latence de queue : moins de collectes gen0 déclenchées par les state machines
Avantages et inconvénients face au modèle async classique
Le point de comparaison, c'est le modèle actuel : la state machine générée par le compilateur, éprouvée depuis C# 5 et présente dans tous les runtimes .NET jusqu'à .NET 10 inclus.
Ce que runtime async apporte
- Aucun changement de code source : tes async et await restent identiques
- Moins d'allocations et une pression GC réduite sur les chemins fortement asynchrones
- Stack traces et profils mémoire enfin lisibles
- Inlining possible jusque dans la compilation ReadyToRun
- Activé par défaut dans les bibliothèques du runtime .NET 11
Ce qui incite à la prudence
- Nécessite .NET 11, qui n'est pas une version LTS, avec une GA au 10 novembre 2026
- Les gains réels n'apparaissent que lorsque tout le graphe d'appels est recompilé
- Les outils qui réécrivent l'IL (AOP, instrumentation, couverture de code) peuvent nécessiter une mise à jour
- Certains profileurs et agents d'APM doivent être validés avant une bascule en production
- Plus de commutateur d'environnement pour revenir en arrière à chaud : la décision se prend à la compilation
- Tes benchmarks de référence sont à refaire : les chiffres publiés ne valent pas pour ta charge
En pratique : active runtime async d'abord sur un service non critique, compare allocations et latence de queue avant et après, et valide ta chaîne d'observabilité. Le gain est réel mais graduel, alors que le risque se concentre entièrement sur l'outillage.