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


163

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

Решение ниже - то, что я использую прямо сейчас, и мой вопрос: как это можно улучшить? Есть ли объект, который уже включает это поведение в 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();
        }
    }
}

5
.Net, как есть встроенные классы, чтобы помочь с этим сценарием. Большинство ответов, перечисленных здесь, устарели. Смотрите самые последние ответы внизу. Посмотрите на многопоточные блокирующие коллекции. Ответы могут быть устаревшими, но это все еще хороший вопрос!
Том

Я думаю, что все еще хорошая идея узнать о Monitor.Wait / Pulse / PulseAll, даже если у нас есть новые параллельные классы в .NET.
thewpfguy

1
Согласитесь с @thewpfguy. Вы захотите понять основные механизмы блокировки за кулисами. Также стоит отметить, что Systems.Collections.Concurrent не существовал до апреля 2010 года, а затем только в Visual Studio 2010 и выше. Определенно не вариант для удержания VS2008 ...
Вик

Если вы читаете это сейчас, взгляните на System.Threading.Channels для ограниченной реализации с несколькими записывающими / множественными читателями, которая может быть дополнительно блокирована для .NET Core и .NET Standard.
Марк Рендл

Ответы:


200

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

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

1
Как насчет изменения ожидания на WaitAny и передачи терминирующей ручки ожидания на стройке ...
Сэм Шафран

1
@ Marc - оптимизация, если вы ожидали, что очередь всегда достигнет емкости, будет состоять в том, чтобы передать значение maxSize в конструктор очереди <T>. Вы можете добавить еще один конструктор в ваш класс, чтобы приспособиться к этому.
RichardOD

3
Почему SizeQueue, а не FixedSizeQueue?
mindless.panda

4
@Lasse - освобождает блокировку Wait, чтобы другие потоки могли ее получить. Он восстанавливает блокировки, когда он просыпается.
Марк Гравелл

1
Хорошо, как я уже сказал, было что-то, чего я не получал :) Это наверняка заставляет меня хотеть пересмотреть некоторые из моих потоковых кодов ...
Лассе В. Карлсен

59

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


14

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

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

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

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

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

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

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

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


12

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



}

3
По умолчанию для BlockingCollection выбрана очередь. Поэтому я не думаю, что это необходимо.
Кертис Уайт

Сохраняет ли BlockingCollection порядок как очередь?
Джоэлк

Да, когда он инициализируется с ConcurrentQueue
Андреас

6

Я просто поднял это с помощью 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;
    }
}

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


Что такое тема <t>? У меня нет резольвера для его пространства имен.
TheJerm

Это часть Реактивных Расширений.
Марк Рендл

Не ответ Это не отвечает на вопрос вообще.
Махдуми

5

Это то, что я пришел, чтобы создать потокобезопасную ограниченную очередь блокировки.

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

Не могли бы вы предоставить несколько примеров кода того, как я бы поставил в очередь некоторые функции потоков, используя эту библиотеку, в том числе, как я бы создал этот класс?
TheJerm

Этот вопрос / ответ немного устарел. Вы должны посмотреть на пространство имен System.Collections.Concurrent для блокировки поддержки очереди.
Кевин

2

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

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


Я знаю, что это старо, но мой комментарий для новичков в SO, так как OP уже знает это сегодня. Это не ответ, это должен был быть комментарий.
Джон Деметриу

0

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


Я посмотрел на это для регулирования количества потоков, которые обращаются к ресурсу, но это не позволяет вам блокировать весь доступ к ресурсу на основании некоторого условия (например, Collection.Count). AFAIK в любом случае
Эрик Шуновер

Ну, вы делаете это сами, как и сейчас. Просто вместо MaxSize и _FullEvent у вас есть семафор, который вы инициализируете с правильным количеством в конструкторе. Затем при каждом добавлении / удалении вы вызываете WaitForOne () или Release ().
Vilx

Это не сильно отличается от того, что у вас есть сейчас. Просто проще ИМХО.
Vilx

Можете ли вы привести пример, показывающий, как это работает? Я не видел, как динамически регулировать размер Semaphor, что требует этот сценарий. Поскольку вы должны иметь возможность блокировать все ресурсы, только если очередь заполнена.
Эрик Шуновер

Ааа, меняется размер! Почему ты не сказал это сразу? Хорошо, тогда семафор не для тебя. Удачи с этим подходом!
Vilx-

-1

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


Я хочу, чтобы никто не смог написать, если очередь заполнена.
Эрик Шуновер

Таким образом, вы комбинируете это с замком. Вот несколько очень хороших примеров: albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
С очередью / очередью каждый является писателем ... эксклюзивная блокировка, возможно, будет более прагматичной
Марк Гравелл

Я знаю, что это старо, но мой комментарий для новичков в SO, так как OP уже знает это сегодня. Это не ответ, это должен был быть комментарий.
Джон Деметриу
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.