我有一个场景,我有多个线程添加到队列和多个线程从同一队列读取。如果队列达到特定大小正在填充队列的所有线程将在添加时被阻止,直到从队列中删除项目为止。
下面的解决方案就是我现在正在使用的问题,我的问题是:如何改进?是否有一个对象已经在我应该使用的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();
}
}
}
答案 0 :(得分:195)
看起来非常不安全(很少同步);怎么样的:
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 :(得分:53)
使用.net 4 BlockingCollection,使用Add()排队使用Take()。它在内部使用非阻塞的ConcurrentQueue。更多信息Fast and Best Producer/consumer queue technique BlockingCollection vs concurrent Queue
答案 2 :(得分:14)
“如何改进?”
好吧,您需要查看类中的每个方法,并考虑如果另一个线程同时调用该方法或任何其他方法会发生什么。例如,您在Remove方法中放置了一个锁,但在Add方法中没有。如果一个线程与另一个线程同时添加删除会发生什么? 糟糕的事情。
还要考虑一个方法可以返回第二个对象,该对象提供对第一个对象的内部数据的访问 - 例如,GetEnumerator。想象一个线程正在通过该枚举器,另一个线程正在同时修改列表。 不好。
一个好的经验法则是通过将类中的方法数量减少到绝对最小值来使这个更简单。
特别是,不要继承另一个容器类,因为您将公开该类的所有方法,为调用者提供破坏内部数据的方法,或者查看部分完整的数据更改(同样糟糕,因为此时数据显示已损坏)。隐藏所有细节,并完全无情地了解如何允许访问它们。
我强烈建议您使用现成的解决方案 - 获取有关线程或使用第三方库的书籍。否则,根据您的尝试,您将长时间调试代码。
此外,对于删除项目(例如,首先添加的项目,因为它是队列)而不是调用者选择特定项目,它是否更有意义?当队列为空时,也许删除也应该阻止。
更新:Marc的答案实际上实现了所有这些建议! :)但是我会把它留在这里,因为它可能有助于理解为什么他的版本是如此的改进。
答案 3 :(得分:8)
您可以使用System.Collections.Concurrent命名空间中的BlockingCollection和ConcurrentQueue
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)
{
}
}
答案 4 :(得分: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;
}
}
不一定完全安全,但非常简单。
答案 5 :(得分: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;
}
}
答案 6 :(得分:2)
我还没有完全探索过TPL,但它们可能有适合您需求的东西,或者至少是一些Reflector饲料可以从中获取灵感。
希望有所帮助。
答案 7 :(得分:0)
好吧,你可以看一下System.Threading.Semaphore
课程。除此之外 - 不,你必须自己做。 AFAIK没有这样的内置系列。
答案 8 :(得分:0)
从.NET 5.0 / Core 3.0开始,您可以使用System.Threading.Channels
this (Asynchronous Producer Consumer Pattern in .NET (C#))文章的基准测试显示,与BlockingCollection相比,速度有了显着提高!
答案 9 :(得分:-1)
如果你想要最大的吞吐量,允许多个读者阅读,只有一个写者可以写,BCL有一个名为ReaderWriterLockSlim的东西应该有助于减少你的代码......