Создание блокирующей очереди <T> в .NET?

У меня есть сценарий, в котором у меня есть несколько потоков, добавляющих очередь и несколько потоков, считающихся из одной очереди. Если очередь достигает определенного размера , все потоки, заполняющие очередь, будут заблокированы при добавлении до тех пор, пока элемент не будет удален из очереди.

Ниже приведено то, что я использую сейчас, и мой вопрос: как это можно улучшить? Есть ли объект, который уже позволяет это поведение в BCL, которое я должен использовать?

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();
        }
    }
}

Ответ 1

Это выглядит очень опасно (очень маленькая синхронизация); как насчет чего-то вроде:

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;
        }
    }
}

(редактировать)

На самом деле вам нужен способ закрыть очередь, чтобы читатели начали выходить чисто - возможно, что-то вроде флага bool - если установлено, пустая очередь просто возвращает (а не блокирует):

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;
    }
}

Ответ 2

Используйте .net 4 BlockingCollection, чтобы установить в очередь использование Add(), чтобы удалить из использования Take(). Он внутренне использует неблокирующий ConcurrentQueue. Подробнее здесь Быстрая и лучшая техника производителя/потребительской очереди BlockingCollection vs параллельная очередь

Ответ 3

"Как это можно улучшить?"

Ну, вам нужно посмотреть на каждый метод в своем классе и подумать, что произойдет, если другой поток одновременно вызовет этот метод или любой другой метод. Например, вы помещаете блокировку в метод Remove, но не в метод Add. Что произойдет, если один поток добавит в то же время, что и другой поток Удаляет? Плохие вещи.

Также подумайте, что метод может возвращать второй объект, который обеспечивает доступ к внутренним данным первого объекта - например, GetEnumerator. Представьте, что один поток проходит через этот счетчик, другой поток одновременно изменяет список. Нехорошо.

Хорошее эмпирическое правило состоит в том, чтобы сделать это проще, если вы сократите количество методов в классе до абсолютного минимума.

В частности, не наследуйте другой класс контейнера, потому что вы будете раскрывать все эти методы класса, предоставляя способ для вызывающего пользователя повредить внутренние данные или просмотреть частично полные изменения данных (так же плохо, потому что в этот момент данные выглядят поврежденными). Скрыть все детали и быть абсолютно беспощадными в отношении того, как вы разрешаете им доступ.

Я настоятельно рекомендую вам использовать готовые решения - получить книгу о потоковом использовании или использовать стороннюю библиотеку. В противном случае, учитывая то, что вы пытаетесь, вы долго будете отлаживать свой код.

Кроме того, не было бы лишним смысл для Remove вернуть элемент (скажем, тот, который был добавлен первым, как очередь), вместо того, чтобы вызывающий выбирал конкретный элемент? И когда очередь пуста, возможно, Remove также должен блокироваться.

Обновление: ответ Marc на самом деле реализует все эти предложения!:) Но я оставлю это здесь, так как может быть полезно понять, почему его версия является таким улучшением.

Ответ 4

Вы можете использовать BlockingCollection и ConcurrentQueue в пространстве имен 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)
    {
    }



}

Ответ 5

Я только что сбил это с помощью Reactive Extensions и вспомнил этот вопрос:

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;
    }
}

Не обязательно полностью безопасный, но очень простой.

Ответ 6

Это то, что я сделал op для потоковой блокировки с ограниченным потоком.

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;
    }
}

Ответ 7

Я не полностью изучил TPL, но у них может быть что-то, что соответствует вашим потребностям или, по крайней мере, некоторым рефлекторным кормам чтобы уловить некоторое вдохновение.

Надеюсь, что это поможет.

Ответ 8

Хорошо, вы можете посмотреть класс System.Threading.Semaphore. Кроме этого - нет, вы должны сделать это сами. AFAIK нет такой встроенной коллекции.

Ответ 9

Если вам нужна максимальная пропускная способность, позволяющая читать несколько читателей и только один писатель писать, BCL имеет нечто, называемое ReaderWriterLockSlim, которое должно помочь уменьшить ваш код...