在.NET中创建阻塞Queue <T>?


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如何具有内置类来帮助解决这种情况。这里列出的大多数答案都是过时的。请参阅底部的最新答案。查看线程安全的阻塞集合。答案可能已过时,但这仍然是一个好问题!
汤姆·阿

我认为,即使我们在.NET中有了新的并发类,了解Monitor.Wait / Pulse / PulseAll仍然是一个好主意。
thewpfguy 2014年

1
同意@thewpfguy。您将需要了解幕后的基本锁定机制。同样值得注意的是,Systems.Collections.Concurrent直到2010年4月才存在,然后才出现在Visual Studio 2010及更高版本中。绝对不是VS2008支持的选项...
Vic

如果您现在正在阅读此内容,请查看适用于.NET Core和.NET Standard的多编写器/多阅读器的System.Threading.Channels,该方法有界,可选地阻塞。
Mark Rendle

Answers:


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 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并在建筑中传递终止的等待句柄……
Sam Saffron

1
@ Marc-如果您期望队列始终达到容量上限,那么可以将maxSize值传递到Queue <T>的构造函数中,这是一种优化。您可以在类中添加另一个构造函数以适应这一点。
查OD10年

3
为什么选择SizeQueue,为什么不选择FixedSizeQueue?
mindless.panda 2010年

4
@Lasse-在期间释放锁Wait,以便其他线程可以获取它。唤醒时,它将收回锁。
马克·格雷夫

1
尼斯,正如我所说的,有些事情我没有得到:)这肯定让我想重温一些我的线程代码....
拉塞·五卡尔森


14

“如何改善?”

好吧,您需要查看类中的每个方法,并考虑如果另一个线程同时调用该方法或任何其他方法会发生什么。例如,您将锁置于Remove方法中,而不是Add方法中。如果一个线程在同时添加而另一个线程在删除,该怎么办?坏事。

还应考虑一种方法可以返回第二个对象,该对象提供对第一个对象的内部数据的访问权限,例如GetEnumerator。想象一个线程正在通过该枚举器,另一个线程正在同时修改列表。不好。

一个好的经验法则是通过将类中的方法数量减少到绝对最小来使这种方法更容易实现。

特别是,不要继承另一个容器类,因为您将公开该类的所有方法,为调用者提供一种破坏内部数据的方式,或查看对数据的部分完成的更改(同样糟糕,因为数据似乎在此时已损坏)。隐藏所有细节,对如何允许访问它们完全无情。

我强烈建议您使用现成的解决方案-获得有关线程的书或使用3rd party库。否则,根据您的尝试,您将需要长时间调试代码。

另外,Remove返回一个项目(例如,由于队列而首先添加的项目),而不是调用者选择特定项目,是否更有意义?而且当队列为空时,也许Remove也应该阻塞。

更新:Marc的答案实际上实现了所有这些建议!:)但我将其保留在此处,因为这可能有助于理解为什么他的版本如此改进。


12

您可以在System.Collections.Concurrent命名空间中使用BlockingCollectionConcurrentQueue

 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默认为Queue。因此,我认为这不是必需的。
柯蒂斯·怀特

BlockingCollection是否像队列一样保留排序?
joelc

是的,当使用ConcurrentQueue进行初始化时
Andreas 2016年

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

不一定完全安全,但非常简单。


什么是Subject <t>?我没有任何名称空间解析器。
theJerm

它是Reactive Extensions的一部分。
Mark Rendle

没有答案。这根本无法回答问题。
makhdumi

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命名空间以阻止队列支持。
凯文(Kevin)

2

我还没有完全探索过TPL,但是它们可能有满足您需求的东西,或者至少是一些Reflector饲料可以从中获取灵感。

希望有帮助。


我知道这很旧,但是我的评论是针对SO的新手的,因为OP今天已经知道了这一点。这不是答案,应该是评论。
John Demetriou

0

好吧,你可能会看System.Threading.Semaphore课。除此之外-不,您必须自己制作。AFAIK没有此类内置集合。


我查看了这些数据,以限制正在访问资源的线程数量,但是它不允许您基于某种条件(例如Collection.Count)阻止所有对资源的访问。无论如何还是AFAIK
Eric Sc​​hoonover,

好吧,就像您现在所做的那样,您需要自己做这部分。只需使用信号量(而不是MaxSize和_FullEvent),即可在构造函数中使用正确的计数对其进行初始化。然后,在每次添加/删除时,您都调用WaitForOne()或Release()。
Vilx-

它与您现在所拥有的没有太大不同。只是更简单的恕我直言。
Vilx-

您能举个例子说明这个工作吗?我没有看到如何动态调整此场景所需的信号量。因为您必须仅在队列已满时才能够阻止所有资源。
Eric Sc​​hoonover,

啊,改变大小!你怎么不马上说 好的,那么信号灯不适合您。祝您好运!
Vilx-

-1

如果您想要最大的吞吐量,允许多个读取器读取而只有一个写入器写入,则BCL有一个称为ReaderWriterLockSlim的东西,可以帮助减少代码的数量。


如果队列已满,我希望没有人能够写。
Eric Sc​​hoonover,

因此,您可以将其与锁结合使用。这是一些非常好的示例 albahari.com/threading/part2.aspx#_ProducerConsumerQWaitHandle albahari.com/threading/part4.aspx
DavidN

3
有了队列/出队,每个人都是作家...排他锁可能更加实用
Marc Gravell

我知道这很旧,但是我的评论是针对SO的新手的,因为OP今天已经知道了这一点。这不是答案,应该是评论。
John Demetriou
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.