Dot.Blog

Consulting DotNet C#, XAML, WinUI, WPF, MAUI, IA

ValueTask, IAsyncEnumerable et Channels en C# : la boîte à outils du traitement asynchrone moderne

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 ?

Faites des heureux, PARTAGEZ l'article !