Creazione di una coda di blocco <T> in .NET?


163

Ho uno scenario in cui ho più thread che si aggiungono a una coda e più thread che leggono dalla stessa coda. Se la coda raggiunge una dimensione specifica, tutti i thread che riempiono la coda verranno bloccati durante l'aggiunta fino a quando un elemento non viene rimosso dalla coda.

La soluzione che segue è quella che sto usando in questo momento e la mia domanda è: come può essere migliorato? Esiste un oggetto che abilita già questo comportamento nel BCL che dovrei usare?

internal class BlockingCollection<T> : CollectionBase, IEnumerable
{
    //todo: might be worth changing this into a proper QUEUE

    private AutoResetEvent _FullEvent = new AutoResetEvent(false);

    internal T this[int i]
    {
        get { return (T) List[i]; }
    }

    private int _MaxSize;
    internal int MaxSize
    {
        get { return _MaxSize; }
        set
        {
            _MaxSize = value;
            checkSize();
        }
    }

    internal BlockingCollection(int maxSize)
    {
        MaxSize = maxSize;
    }

    internal void Add(T item)
    {
        Trace.WriteLine(string.Format("BlockingCollection add waiting: {0}", Thread.CurrentThread.ManagedThreadId));

        _FullEvent.WaitOne();

        List.Add(item);

        Trace.WriteLine(string.Format("BlockingCollection item added: {0}", Thread.CurrentThread.ManagedThreadId));

        checkSize();
    }

    internal void Remove(T item)
    {
        lock (List)
        {
            List.Remove(item);
        }

        Trace.WriteLine(string.Format("BlockingCollection item removed: {0}", Thread.CurrentThread.ManagedThreadId));
    }

    protected override void OnRemoveComplete(int index, object value)
    {
        checkSize();
        base.OnRemoveComplete(index, value);
    }

    internal new IEnumerator GetEnumerator()
    {
        return List.GetEnumerator();
    }

    private void checkSize()
    {
        if (Count < MaxSize)
        {
            Trace.WriteLine(string.Format("BlockingCollection FullEvent set: {0}", Thread.CurrentThread.ManagedThreadId));
            _FullEvent.Set();
        }
        else
        {
            Trace.WriteLine(string.Format("BlockingCollection FullEvent reset: {0}", Thread.CurrentThread.ManagedThreadId));
            _FullEvent.Reset();
        }
    }
}

5
.Net come ha classi integrate per aiutare con questo scenario. La maggior parte delle risposte elencate qui sono obsolete. Vedi le risposte più recenti in fondo. Cerca nelle raccolte di blocchi thread-safe. Le risposte possono essere obsolete, ma è comunque una buona domanda!
Tom A

Penso che sia ancora una buona idea conoscere Monitor.Wait / Pulse / PulseAll anche se abbiamo nuove classi simultanee in .NET.
thewpfguy

1
Sono d'accordo con @thewpfguy. Ti consigliamo di comprendere i meccanismi di blocco di base dietro le quinte. Vale anche la pena notare che Systems.Collections.Concurrent non esisteva fino ad aprile 2010 e quindi solo in Visual Studio 2010 e versioni successive. Sicuramente non è un'opzione per gli hold out del VS2008 ...
Vic

Se stai leggendo questo adesso, dai un'occhiata a System.Threading.Channels per un'implementazione multi-writer / multi-reader, limitata, che blocca facoltativamente questo per .NET Core e .NET Standard.
Mark Rendle,

Risposte:


200

Sembra molto pericoloso (pochissima sincronizzazione); che ne dici di qualcosa come:

class SizeQueue<T>
{
    private readonly Queue<T> queue = new Queue<T>();
    private readonly int maxSize;
    public SizeQueue(int maxSize) { this.maxSize = maxSize; }

    public void Enqueue(T item)
    {
        lock (queue)
        {
            while (queue.Count >= maxSize)
            {
                Monitor.Wait(queue);
            }
            queue.Enqueue(item);
            if (queue.Count == 1)
            {
                // wake up any blocked dequeue
                Monitor.PulseAll(queue);
            }
        }
    }
    public T Dequeue()
    {
        lock (queue)
        {
            while (queue.Count == 0)
            {
                Monitor.Wait(queue);
            }
            T item = queue.Dequeue();
            if (queue.Count == maxSize - 1)
            {
                // wake up any blocked enqueue
                Monitor.PulseAll(queue);
            }
            return item;
        }
    }
}

(modificare)

In realtà, vorresti un modo per chiudere la coda in modo che i lettori iniziassero a uscire in modo pulito - forse qualcosa come una bandiera bool - se impostato, una coda vuota restituisce (piuttosto che bloccare):

bool closing;
public void Close()
{
    lock(queue)
    {
        closing = true;
        Monitor.PulseAll(queue);
    }
}
public bool TryDequeue(out T value)
{
    lock (queue)
    {
        while (queue.Count == 0)
        {
            if (closing)
            {
                value = default(T);
                return false;
            }
            Monitor.Wait(queue);
        }
        value = queue.Dequeue();
        if (queue.Count == maxSize - 1)
        {
            // wake up any blocked enqueue
            Monitor.PulseAll(queue);
        }
        return true;
    }
}

1
Che ne dici di cambiare l'attesa in un WaitAny e passare in una maniglia di attesa terminata in costruzione ...
Sam Saffron

1
@ Marc- un'ottimizzazione, se ti aspettavi che la coda raggiungesse sempre la capacità, sarebbe quella di passare il valore maxSize nel costruttore della coda <T>. Potresti aggiungere un altro costruttore alla tua classe per adattarlo.
RichardOD,

3
Perché SizeQueue, perché non FixedSizeQueue?
mindless.panda,

4
@Lasse: rilascia i blocchi durante Wait, quindi altri thread possono acquisirlo. Recupera i lucchetti quando si sveglia.
Marc Gravell

1
Bello, come ho detto, c'era qualcosa che non stavo ottenendo :) Questo mi fa sicuramente venire voglia di rivisitare un po 'del mio codice thread ....
Lasse V. Karlsen


14

"Come può essere migliorato?"

Bene, devi esaminare tutti i metodi della tua classe e considerare cosa accadrebbe se un altro thread chiamasse contemporaneamente quel metodo o qualsiasi altro metodo. Ad esempio, si inserisce un blocco nel metodo Rimuovi, ma non nel metodo Aggiungi. Cosa succede se un thread viene aggiunto contemporaneamente a un altro thread? Cose brutte.

Considera anche che un metodo può restituire un secondo oggetto che fornisce l'accesso ai dati interni del primo oggetto, ad esempio GetEnumerator. Immagina che un thread attraversi quell'enumeratore, un altro thread sta modificando l'elenco contemporaneamente. Non bene.

Una buona regola empirica è rendere questo più semplice da ottenere riducendo il numero di metodi della classe al minimo assoluto.

In particolare, non ereditare un'altra classe contenitore, perché esporrete tutti i metodi di quella classe, fornendo un modo per il chiamante di corrompere i dati interni o di vedere modifiche parzialmente complete ai dati (altrettanto male, perché i dati sembra corrotto in quel momento). Nascondi tutti i dettagli e sii completamente spietato su come permetterne l'accesso.

Ti consiglio vivamente di utilizzare soluzioni standardizzate: ottenere un libro sul threading o utilizzare una libreria di terze parti. Altrimenti, dato quello che stai tentando, eseguirai il debug del tuo codice per molto tempo.

Inoltre, non avrebbe più senso per Rimuovi restituire un elemento (ad esempio, quello che è stato aggiunto per primo, in quanto è una coda), piuttosto che il chiamante che sceglie un elemento specifico? E quando la coda è vuota, forse Rimuovi dovrebbe anche bloccare.

Aggiornamento: la risposta di Marc implementa effettivamente tutti questi suggerimenti! :) Ma lo lascerò qui perché potrebbe essere utile capire perché la sua versione è un tale miglioramento.


12

È possibile utilizzare BlockingCollection e ConcurrentQueue nello spazio dei nomi System.Collections.Concurrent

 public class ProducerConsumerQueue<T> : BlockingCollection<T>
{
    /// <summary>
    /// Initializes a new instance of the ProducerConsumerQueue, Use Add and TryAdd for Enqueue and TryEnqueue and Take and TryTake for Dequeue and TryDequeue functionality
    /// </summary>
    public ProducerConsumerQueue()  
        : base(new ConcurrentQueue<T>())
    {
    }

  /// <summary>
  /// Initializes a new instance of the ProducerConsumerQueue, Use Add and TryAdd for Enqueue and TryEnqueue and Take and TryTake for Dequeue and TryDequeue functionality
  /// </summary>
  /// <param name="maxSize"></param>
    public ProducerConsumerQueue(int maxSize)
        : base(new ConcurrentQueue<T>(), maxSize)
    {
    }



}

3
BlockingCollection è impostato automaticamente su Coda. Quindi, non penso sia necessario.
Curtis White,

BlockingCollection conserva l'ordinamento come una coda?
Joelc,

Sì, quando viene inizializzato con ConcurrentQueue
Andreas,

6

Ho appena rovinato tutto usando le estensioni reattive e mi sono ricordato di questa domanda:

public class BlockingQueue<T>
{
    private readonly Subject<T> _queue;
    private readonly IEnumerator<T> _enumerator;
    private readonly object _sync = new object();

    public BlockingQueue()
    {
        _queue = new Subject<T>();
        _enumerator = _queue.GetEnumerator();
    }

    public void Enqueue(T item)
    {
        lock (_sync)
        {
            _queue.OnNext(item);
        }
    }

    public T Dequeue()
    {
        _enumerator.MoveNext();
        return _enumerator.Current;
    }
}

Non necessariamente del tutto sicuro, ma molto semplice.


Che cos'è il soggetto <t>? Non ho alcun resolver per il suo spazio dei nomi.
theJerm

Fa parte delle estensioni reattive.
Mark Rendle,

Non una risposta Questo non risponde affatto alla domanda.
makhdumi,

5

Questo è quello che mi è venuto in mente per una coda di blocco limitata da thread.

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

public class BlockingBuffer<T>
{
    private Object t_lock;
    private Semaphore sema_NotEmpty;
    private Semaphore sema_NotFull;
    private T[] buf;

    private int getFromIndex;
    private int putToIndex;
    private int size;
    private int numItems;

    public BlockingBuffer(int Capacity)
    {
        if (Capacity <= 0)
            throw new ArgumentOutOfRangeException("Capacity must be larger than 0");

        t_lock = new Object();
        buf = new T[Capacity];
        sema_NotEmpty = new Semaphore(0, Capacity);
        sema_NotFull = new Semaphore(Capacity, Capacity);
        getFromIndex = 0;
        putToIndex = 0;
        size = Capacity;
        numItems = 0;
    }

    public void put(T item)
    {
        sema_NotFull.WaitOne();
        lock (t_lock)
        {
            while (numItems == size)
            {
                Monitor.Pulse(t_lock);
                Monitor.Wait(t_lock);
            }

            buf[putToIndex++] = item;

            if (putToIndex == size)
                putToIndex = 0;

            numItems++;

            Monitor.Pulse(t_lock);

        }
        sema_NotEmpty.Release();


    }

    public T take()
    {
        T item;

        sema_NotEmpty.WaitOne();
        lock (t_lock)
        {

            while (numItems == 0)
            {
                Monitor.Pulse(t_lock);
                Monitor.Wait(t_lock);
            }

            item = buf[getFromIndex++];

            if (getFromIndex == size)
                getFromIndex = 0;

            numItems--;

            Monitor.Pulse(t_lock);

        }
        sema_NotFull.Release();

        return item;
    }
}

Potresti fornire alcuni esempi di codice di come metterei in coda alcune funzioni di thread usando questa libreria, incluso come farei un'istanza di questa classe?
theJerm

Questa domanda / risposta è un po 'datata. È necessario esaminare lo spazio dei nomi System.Collections.Concurrent per bloccare il supporto delle code.
Kevin,

2

Non ho esplorato completamente il TPL, ma potrebbero avere qualcosa che si adatta alle tue esigenze, o almeno, un foraggio Reflector per trarre ispirazione.

Spero che aiuti.


Sono consapevole che questo è vecchio, ma il mio commento è per i neofiti di SO, poiché OP lo sa già oggi. Questa non è una risposta, questo avrebbe dovuto essere un commento.
John Demetriou,

0

Bene, potresti guardare la System.Threading.Semaphorelezione. A parte questo, no, devi farlo tu stesso. AFAIK non esiste una tale collezione integrata.


L'ho osservato per limitare il numero di thread che accedono a una risorsa ma non consente di bloccare tutto l'accesso a una risorsa in base a una condizione (come Collection.Count). AFAIK comunque
Eric Schoonover,

Bene, fai quella parte, proprio come fai adesso. Semplicemente invece di MaxSize e _FullEvent hai il semaforo, che inizializzi con il giusto conteggio nel costruttore. Quindi, ad ogni Aggiungi / Rimuovi chiami WaitForOne () o Release ().
Vilx-

Non è molto diverso da quello che hai ora. Solo IMHO più semplice.
Vilx-

Puoi darmi un esempio che mostra questo lavoro? Non ho visto come regolare dinamicamente le dimensioni di una semafora che questo scenario richiede. Dal momento che devi essere in grado di bloccare tutte le risorse solo se la coda è piena.
Eric Schoonover,

Ah, cambiando taglia! Perché non l'hai detto subito? OK, allora un semaforo non fa per te. Buona fortuna con questo approccio!
Vilx-

-1

Se vuoi il massimo rendimento, consentendo a più lettori di leggere e solo uno scrittore di scrivere, BCL ha qualcosa chiamato ReaderWriterLockSlim che dovrebbe aiutarti a snellire il tuo codice ...


Non voglio che nessuno sia in grado di scrivere se la coda è piena però.
Eric Schoonover,


3
Con queue / dequeue, ognuno è uno scrittore ... un blocco esclusivo sarà forse più pragmatico
Marc Gravell

Sono consapevole che questo è vecchio, ma il mio commento è per i neofiti di SO, poiché OP lo sa già oggi. Questa non è una risposta, questo avrebbe dovuto essere un commento.
John Demetriou,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.