🏭 Définition
-
Producteurs : génèrent des données (ou tâches) et les déposent dans une file partagée.
-
Consommateurs : récupèrent les données depuis cette file et les traitent.
-
La file sert de tampon entre les deux.
Cela permet de découpler la vitesse des producteurs et des consommateurs :
-
si les producteurs vont plus vite → les données s’accumulent dans la file.
-
si les consommateurs vont plus vite → ils attendent qu’il y ait des données disponibles.
🔑 Objectif
-
Éviter de bloquer inutilement un thread.
-
Lisser les différences de cadence entre production et consommation.
-
Simplifier la synchronisation : les producteurs n’ont pas à connaître les consommateurs, et inversement.
⚙️ Implémentation en C#
1. Avec BlockingCollection<T> (solution moderne et simple)
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var buffer = new BlockingCollection<int>(boundedCapacity: 5);
// Producteur
var producer = Task.Run(() =>
{
for (int i = 1; i <= 10; i++)
{
buffer.Add(i);
Console.WriteLine($"[Producteur] Ajout {i}");
Thread.Sleep(200); // simule un calcul
}
buffer.CompleteAdding(); // signale la fin
});
// Consommateur
var consumer = Task.Run(() =>
{
foreach (var item in buffer.GetConsumingEnumerable())
{
Console.WriteLine($" [Consommateur] Traite {item}");
Thread.Sleep(500); // simule un traitement plus long
}
});
Task.WaitAll(producer, consumer);
Console.WriteLine("Terminé !");
}
}
2. Fonctionnement de l’exemple :
-
Le producteur ajoute les nombres
1..10dans la collection. -
Le consommateur lit et traite plus lentement → la file joue le rôle de tampon.
-
Quand le producteur a fini →
CompleteAdding()notifie le consommateur qu’il n’y aura plus de nouveaux éléments.
📊 Schéma du pattern
[ Producteurs ] ---> [ File partagée (BlockingCollection<T>) ] ---> [ Consommateurs ]
(rapides) (tampon) (lents)
🧩 Avantages
-
Découplage producteur / consommateur.
-
Équilibrage automatique des vitesses.
-
Réduit la contention (pas besoin de
lockmanuel). -
Facile à implémenter avec
BlockingCollection<T>ouChannels(C# 8+).
📦 1. BlockingCollection<T>
-
Introduit avec .NET 4.0.
-
Une collection thread-safe qui sert de tampon entre producteurs et consommateurs.
-
Basée sur une collection sous-jacente (
ConcurrentQueue<T>,ConcurrentStack<T>, etc.). -
Bloque automatiquement :
-
les producteurs quand la collection est pleine (si capacité définie).
-
les consommateurs quand elle est vide.
-
-
Fournit des méthodes utiles :
-
Add(item)→ ajoute un élément (bloque si plein). -
Take()→ prend un élément (bloque si vide). -
GetConsumingEnumerable()→ énumération bloquante pratique pour les consommateurs. -
CompleteAdding()→ indique qu’il n’y aura plus de nouveaux éléments.
-
👉 Idéal pour implémenter rapidement le pattern Producteur/Consommateur.
📡 2. Channels (System.Threading.Channels)
-
Introduit avec .NET Core 3.0 (et dispo en .NET 5+).
-
Inspiré de Go et Rust → très moderne.
-
Fournit un canal de communication asynchrone entre producteurs et consommateurs.
-
Supporte nativement l’async/await (
WriteAsync,ReadAsync). -
Plus performant et flexible que
BlockingCollection<T>. -
Deux types principaux :
-
Channel.Unbounded<T>()→ capacité illimitée. -
Channel.Bounded<T>(capacity)→ capacité limitée, avec options (drop oldest, block, etc.).
-
👉 Recommandé dans les applis modernes async/await (ex: ASP.NET Core, services haute perf).
⚖️ Comparaison rapide
| Caractéristique | BlockingCollection | Channels |
|---|---|---|
| Année d’introduction | .NET 4.0 (2010) | .NET Core 3.0 (2019) |
| Style de code | Synchrone | Asynchrone (async/await) |
| Base technique | Collections concurrentes | Channels optimisés bas niveau |
| Tampon (bounded) | Oui | Oui (plus configurable) |
| Utilisation conseillée | Applis console, jobs simples | Applis modernes async, services web |
🖥️ Exemple avec BlockingCollection<T>
var buffer = new BlockingCollection<int>(5);
// Producteur
Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
buffer.Add(i);
Console.WriteLine($"Produit {i}");
}
buffer.CompleteAdding();
});
// Consommateur
foreach (var item in buffer.GetConsumingEnumerable())
{
Console.WriteLine($" Consommé {item}");
}
🖥️ Exemple avec Channel<T>
using System;
using System.Threading.Channels;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var channel = Channel.CreateBounded<int>(5);
// Producteur
_ = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
Console.WriteLine($"Produit {i}");
}
channel.Writer.Complete();
});
// Consommateur
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($" Consommé {item}");
}
}
}
👉 Donc :
-
BlockingCollection<T>= simple, synchrone, parfait pour des applis console ou du code classique. -
Channels= moderne, asynchrone, recommandé dans les applis .NET Core / .NET 5+ (surtout avecasync/await).
Aucun commentaire:
Enregistrer un commentaire