web-dev-qa-db-fra.com

Producteur / consommateur C #

j'ai récemment rencontré une implémentation de modèle de producteur/consommateur c #. c'est très simple et (pour moi du moins) très élégant.

il semble avoir été conçu vers 2006, donc je me demandais si cette mise en œuvre est
- sûr
- toujours applicable

Le code est ci-dessous (le code d'origine était référencé à http://bytes.com/topic/net/answers/575276-producer-consumer#post2251375 )

using System;  
using System.Collections;  
using System.Threading;

public class Test
{  
    static ProducerConsumer queue;

    static void Main()
    {
        queue = new ProducerConsumer();
        new Thread(new ThreadStart(ConsumerJob)).Start();

        Random rng = new Random(0);
        for (int i=0; i < 10; i++)
        {
            Console.WriteLine ("Producing {0}", i);
            queue.Produce(i);
            Thread.Sleep(rng.Next(1000));
        }
    }

    static void ConsumerJob()
    {
        // Make sure we get a different random seed from the
        // first thread
        Random rng = new Random(1);
        // We happen to know we've only got 10 
        // items to receive
        for (int i=0; i < 10; i++)
        {
            object o = queue.Consume();
            Console.WriteLine ("\t\t\t\tConsuming {0}", o);
            Thread.Sleep(rng.Next(1000));
        }
    }
}

public class ProducerConsumer
{
    readonly object listLock = new object();
    Queue queue = new Queue();

    public void Produce(object o)
    {
        lock (listLock)
        {
            queue.Enqueue(o);

            // We always need to Pulse, even if the queue wasn't
            // empty before. Otherwise, if we add several items
            // in quick succession, we may only Pulse once, waking
            // a single thread up, even if there are multiple threads
            // waiting for items.            
            Monitor.Pulse(listLock);
        }
    }

    public object Consume()
    {
        lock (listLock)
        {
            // If the queue is empty, wait for an item to be added
            // Note that this is a while loop, as we may be pulsed
            // but not wake up before another thread has come in and
            // consumed the newly added object. In that case, we'll
            // have to wait for another Pulse.
            while (queue.Count==0)
            {
                // This releases listLock, only reacquiring it
                // after being woken up by a call to Pulse
                Monitor.Wait(listLock);
            }
            return queue.Dequeue();
        }
    }
}
26
lboregard

Le code est plus ancien que cela - je l'ai écrit quelque temps avant la sortie de .NET 2.0. Le concept d'une file d'attente producteur/consommateur est façon plus ancien que cela :)

Oui, ce code est sûr pour autant que je sache - mais il présente certaines lacunes:

  • Ce n'est pas générique. Une version moderne serait certainement générique.
  • Il n'a aucun moyen d'arrêter la file d'attente. Une façon simple d'arrêter la file d'attente (de sorte que tous les threads consommateurs se retirent) consiste à disposer d'un jeton "Stop Work" qui peut être placé dans la file d'attente. Vous ajoutez ensuite autant de jetons que de threads. Alternativement, vous avez un indicateur distinct pour indiquer que vous souhaitez arrêter. (Cela permet aux autres threads de s'arrêter avant de terminer tout le travail en cours dans la file d'attente.)
  • Si les emplois sont très petits, consommer un seul emploi à la fois n'est peut-être pas la chose la plus efficace à faire.

Pour être honnête, les idées derrière le code sont plus importantes que le code lui-même.

33
Jon Skeet

Vous pouvez faire quelque chose comme l'extrait de code suivant. Il est générique et dispose d'une méthode pour mettre en file d'attente les valeurs nulles (ou tout autre indicateur que vous souhaitez utiliser) pour indiquer aux threads de travail de quitter.

Le code est extrait d'ici: http://www.albahari.com/threading/part4.aspx#_Wait_and_Pulse

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{

    public class TaskQueue<T> : IDisposable where T : class
    {
        object locker = new object();
        Thread[] workers;
        Queue<T> taskQ = new Queue<T>();

        public TaskQueue(int workerCount)
        {
            workers = new Thread[workerCount];

            // Create and start a separate thread for each worker
            for (int i = 0; i < workerCount; i++)
                (workers[i] = new Thread(Consume)).Start();
        }

        public void Dispose()
        {
            // Enqueue one null task per worker to make each exit.
            foreach (Thread worker in workers) EnqueueTask(null);
            foreach (Thread worker in workers) worker.Join();
        }

        public void EnqueueTask(T task)
        {
            lock (locker)
            {
                taskQ.Enqueue(task);
                Monitor.PulseAll(locker);
            }
        }

        void Consume()
        {
            while (true)
            {
                T task;
                lock (locker)
                {
                    while (taskQ.Count == 0) Monitor.Wait(locker);
                    task = taskQ.Dequeue();
                }
                if (task == null) return;         // This signals our exit
                Console.Write(task);
                Thread.Sleep(1000);              // Simulate time-consuming task
            }
        }
    }
}
28
dashton

Dans la journée, j'ai appris comment fonctionne Monitor.Wait/Pulse (et beaucoup sur les threads en général) à partir du morceau de code ci-dessus et du série d'articles d'où il vient. Donc, comme le dit Jon, cela a beaucoup de valeur et est en effet sûr et applicable.

Cependant, à partir de .NET 4, il existe une implémentation de file d'attente producteur-consommateur dans le cadre . Je viens juste de le trouver moi-même, mais jusqu'à présent, il fait tout ce dont j'ai besoin.

17
kicsit

Attention: Si vous lisez les commentaires, vous comprendrez que ma réponse est fausse :)

Il y a un possible deadlock dans votre code.

Imaginez le cas suivant, pour plus de clarté, j'ai utilisé une approche à un seul thread mais devrait être facile à convertir en multi-thread avec sommeil:

// We create some actions...
object locker = new object();

Action action1 = () => {
    lock (locker)
    {
        System.Threading.Monitor.Wait(locker);
        Console.WriteLine("This is action1");
    }
};

Action action2 = () => {
    lock (locker)
    {
        System.Threading.Monitor.Wait(locker);
        Console.WriteLine("This is action2");
    }
};

// ... (stuff happens, etc.)

// Imagine both actions were running
// and there's 0 items in the queue

// And now the producer kicks in...
lock (locker)
{
    // This would add a job to the queue

    Console.WriteLine("Pulse now!");
    System.Threading.Monitor.Pulse(locker);
}

// ... (more stuff)
// and the actions finish now!

Console.WriteLine("Consume action!");
action1(); // Oops... they're locked...
action2();

Veuillez me le faire savoir si cela n'a aucun sens.

Si cela est confirmé, la réponse à votre question est: "non, ce n'est pas sûr";) J'espère que cela vous aidera.

0
DiogoNeves