vendredi 26 septembre 2025

Le pattern Producteur/Consommateur



🏭 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..10 dans 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 lock manuel).

  • Facile à implémenter avec BlockingCollection<T> ou Channels (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 avec async/await).


Aucun commentaire:

Enregistrer un commentaire