Le développement asynchrone en C# ne se limite plus à Task et async/await. Depuis plusieurs versions, de nouveaux outils puissants sont apparus : ValueTask, IAsyncEnumerable<T> et Channel<T>. Avec .NET 9, ces outils sont devenus des standards pour les traitements performants, continus et faiblement allouants.
Voici un tour d’horizon pragmatique de ces trois outils, avec exemples et cas d’usage concrets.
🔁 ValueTask : éviter des allocations inutiles
Un Task standard alloue toujours un objet sur le heap, même quand le résultat est immédiat. ValueTask permet de contourner ce problème :
public ValueTask<int> GetMagicNumberAsync()
{
return new ValueTask<int>(42); // pas de Task créée
}
Bonnes pratiques :
- Utiliser ValueTask quand une opération peut parfois être synchrone
- Ne jamais "oublier" de l’attendre : toujours l’utiliser via await
⚠️ À éviter : retourner un ValueTask quand l’opération est toujours async. Cela ajoute de la complexité inutile.
🌊 IAsyncEnumerable<T> : lire un flux de données asynchrone
Idéal pour :
- Lecture de fichiers ligne à ligne
- Consommation d’un flux HTTP ou WebSocket
- Streaming d’événements ou logs
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
while (!reader.EndOfStream)
{
yield return await reader.ReadLineAsync();
}
}
await foreach (var line in ReadLinesAsync("data.txt"))
{
Console.WriteLine(line);
}
✅ Très efficace pour éviter les listes en mémoire et streamer en continu.
🔄 Channel<T> : une file d’attente thread-safe, sans allocation
Channel<T> est un outil du namespace System.Threading.Channels. Il permet :
- De connecter deux threads ou producteurs/consommateurs
- De traiter des données en continu avec faible latence
var channel = Channel.CreateUnbounded<int>();
// Producteur
_ = Task.Run(async () =>
{
for (int i = 0; i < 100; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
// Consommateur
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consommé : {item}");
}
✅ Plus performant que BlockingCollection<T> et 100 % async
🧠 Quand utiliser quoi ?
|
Besoin
|
Outil recommandé
|
|
Retour rapide potentiellement sync
|
ValueTask<T>
|
|
Lecture ou génération en flux
|
IAsyncEnumerable<T>
|
|
Communication entre tâches
|
Channel<T>
|
⚠️ Pièges courants
- ValueTask ne peut être awaité qu’une seule fois
- Ne pas oublier de Complete() le writer d’un channel
- Une méthode async avec yield return doit être IAsyncEnumerable<T>, pas Task<IEnumerable<T>>
🔚 Conclusion
Ces trois outils sont essentiels pour construire des pipelines asynchrones modernes en .NET. Ils réduisent les allocations, améliorent la réactivité et rendent le code plus modulaire.
Avec .NET 9, leur intégration est fluide dans tous types de projets (console, desktop, backend, services locaux). Le futur est au streaming, au découplage, et à la gestion fine des ressources. Il ne faut en effet pas s'attendre à ce que tout le monde dispose d'un ordinateur quantique d'ici demain, même après demain, alors qu'il faut s'attendre à une demande toujours plus pressante des utilisateurs pour plus de fonctionnalités, plus de performances et de fluidité. C'est donc à nous, à l'aide de .NET, de combler ce gap. Mais n'est-ce pas ce qui justifie en bonne partie notre salaire, savoir quoi faire, quand le faire et comment le faire ?