Files d'attente de tâches pour C# (.NET) : System.Threading.Channels et TPL Dataflow
Lors du développement d'applications multithread en C# (.NET), il est souvent nécessaire d'organiser le transfert asynchrone de données entre les producteurs (producers) et les consommateurs (consumers). Le modèle classique Producer-Consumer est résolu à l'aide de files d'attente de tâches. Dans l'écosystème .NET, il existe deux outils puissants pour cela : System.Threading.Channels (apparu dans .NET Core 3.0) et la bibliothèque TPL Dataflow (partie de .NET depuis la version 4.5).
Dans cet article, nous analyserons les deux approches, leurs caractéristiques et fournirons des exemples de code pratiques.
1. System.Threading.Channels — une file d'attente légère et rapide
L'espace de noms System.Threading.Channels fournit des files d'attente asynchrones et thread-safe, optimisées pour les scénarios à haut débit. Les principaux types :
Channel<T>— classe abstraite du canal.Channel.CreateUnbounded<T>()— file d'attente illimitée (risque de saturation mémoire).Channel.CreateBounded<T>(int capacity)— file d'attente limitée avec contrôle de pression.ChannelWriter<T>— interface pour l'écriture.ChannelReader<T>— interface pour la lecture.
Exemple : Producer-Consumer avec Channel
Créons un exemple simple où un producteur génère des nombres et un consommateur les traite.
using System.Threading.Channels;using System.Threading.Tasks;
var channel = Channel.CreateUnbounded<int>();
// Producervar producer = Task.Run(async () =>{ for (int i = 0; i < 10; i++) { await channel.Writer.WriteAsync(i); Console.WriteLine($"Produit : {i}"); await Task.Delay(100); } channel.Writer.Complete(); // Signal de fin});
// Consumervar consumer = Task.Run(async () =>{ await foreach (var item in channel.Reader.ReadAllAsync()) { Console.WriteLine($"Traité : {item * 2}"); await Task.Delay(200); }});
await Task.WhenAll(producer, consumer);Console.WriteLine("Terminé !");Avantages de Channels :
- Frais généraux minimaux (sans allocation d'objets supplémentaires).
- Prise en charge intégrée de
async/awaitetIAsyncEnumerable. - Idéal pour les systèmes à forte charge (par exemple, traitement de paquets réseau).
2. TPL Dataflow — un pipeline de traitement puissant
La bibliothèque System.Threading.Tasks.Dataflow fournit des blocs plus complexes pour construire des pipelines et des réseaux de traitement de données. Les principaux blocs :
BufferBlock<T>— une file d'attente simple.ActionBlock<T>— bloc exécutant une action sur chaque élément.TransformBlock<TInput, TOutput>— transforme chaque élément.BatchBlock<T>— regroupe les éléments en lots.
Exemple : Pipeline avec ActionBlock
Réalisons la même tâche : le producteur envoie des nombres, le consommateur les traite.
using System.Threading.Tasks.Dataflow;
var actionBlock = new ActionBlock<int>(async item =>{ await Task.Delay(200); Console.WriteLine($"Traité : {item * 2}");}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 2 });
// Producerfor (int i = 0; i < 10; i++){ await actionBlock.SendAsync(i); Console.WriteLine($"Produit : {i}"); await Task.Delay(100);}
actionBlock.Complete();await actionBlock.Completion;Console.WriteLine("Terminé !");Avantages de TPL Dataflow :
- Configuration flexible