我正在尝试实现支持消费者超时的并发生产者 - 消费者集合(多个生产者和消费者)。
现在实际的集合非常复杂(不幸的是,System.Collections.Concurrent中没有任何内容可以完成这项工作),但我这里有一个最小的示例来演示我的问题(看起来有点像BlockingCollection<T>
)。
public sealed class ProducerConsumerQueueDraft<T>
{
private readonly Queue<T> queue = new Queue<T>();
private readonly object locker = new object();
public void Enqueue(T item)
{
lock (locker)
{
queue.Enqueue(item);
/* This "optimization" is broken, as Nicholas Butler points out.
if(queue.Count == 1) // Optimization
*/
Monitor.Pulse(locker); // Notify any waiting consumer threads.
}
}
public T Dequeue(T item)
{
lock (locker)
{
// Surprisingly, this needs to be a *while* and not an *if*
// which is the core of my problem.
while (queue.Count == 0)
Monitor.Wait(locker);
return queue.Dequeue();
}
}
// This isn't thread-safe, but is how I want TryDequeue to look.
public bool TryDequeueDesired(out T item, TimeSpan timeout)
{
lock (locker)
{
if (queue.Count == 0 && !Monitor.Wait(locker, timeout))
{
item = default(T);
return false;
}
// This is wrong! The queue may be empty even though we were pulsed!
item = queue.Dequeue();
return true;
}
}
// Has nasty timing-gymnastics I want to avoid.
public bool TryDequeueThatWorks(out T item, TimeSpan timeout)
{
lock (locker)
{
var watch = Stopwatch.StartNew();
while (queue.Count == 0)
{
var remaining = timeout - watch.Elapsed;
if (!Monitor.Wait(locker, remaining < TimeSpan.Zero ? TimeSpan.Zero : remaining))
{
item = default(T);
return false;
}
}
item = queue.Dequeue();
return true;
}
}
}
这个想法很简单:找到空队列的消费者等待发出信号,生产者Pulse
(注意:不是PulseAll
,这将是低效的)他们通知他们等待的项目。
我的问题是Monitor.Pulse
的这个属性:
当调用Pulse的线程释放锁定时,下一个 就绪队列中的线程(不一定是那个线程 是脉冲的获得了锁。
这意味着消费者线程C1可以被生产者线程唤醒以消耗一个项目,但另一个消费者线程C2可以在C1有机会重新获取之前获取锁定它,并使用该项,当给予控制时C1留空队列。
这意味着如果队列确实非空,我必须在每个脉冲上以防御性方式检查消费者代码,并且如果不是这样的情况,则返回并空手等待。
我的主要问题是它效率低下 - 线程可能会被唤醒以进行工作,然后立即返回等待。这样做的一个相关结果是,实现具有超时的TryDequeue
是不必要的困难和低效(请参阅TryDequeueThatWorks
)何时应该优雅(请参阅TryDequeueDesired
)。
如何扭转Monitor.Pulse
来做我想要的事情?或者,有另一个同步原语吗?实现TryDequeue
暂停比我做的更有效和/或更优雅吗?
仅供参考,这是一个测试,用于演示我所需解决方案的问题:
var queue = new ProducerConsumerQueueDraft<int>();
for (int consumer = 0; consumer < 3; consumer++)
new Thread(() =>
{
while (true)
{
int item;
// This call should occasionally throw an exception.
// Switching to queue.TryDequeueThatWorks should make
// the problem go away.
if (queue.TryDequeueDesired(out item, TimeSpan.FromSeconds(1)))
{
// Do nothing.
}
}
}).Start();
Thread.Sleep(1000); // Let consumers get up and running
for (int itemIndex = 0; itemIndex < 50000000; itemIndex++)
{
queue.Enqueue(0);
}
答案 0 :(得分:2)
我的主要问题是效率低下
不是。你认为这是一种常见现象,但这种种族很少发生非常。一旦进入蓝月亮,充其量。当 发生时,while循环是必要的,以确保没有出错。它会。不要乱用它。
实际上相反,锁设计效率因为它确实允许竞赛发生。处理它。摆弄锁定设计是非常危险的,因为比赛不会经常发生。它们是非常随机的,它阻止了足够的测试来证明 这些改变不会导致失败。添加任何仪器代码也不起作用,它会改变时间。
答案 1 :(得分:1)
这是一个简单的基于密钥的混合生产者 - 消费者队列:
public class ConflatingConcurrentQueue<TKey, TValue>
{
private readonly ConcurrentDictionary<TKey, Entry> entries;
private readonly BlockingCollection<Entry> queue;
public ConflatingConcurrentQueue()
{
this.entries = new ConcurrentDictionary<TKey, Entry>();
this.queue = new BlockingCollection<Entry>();
}
public void Enqueue(TValue value, Func<TValue, TKey> keySelector)
{
// Get the entry for the key. Create a new one if necessary.
Entry entry = entries.GetOrAdd(keySelector(value), k => new Entry());
// Get exclusive access to the entry.
lock (entry)
{
// Replace any old value with the new one.
entry.Value = value;
// Add the entry to the queue if it's not enqueued yet.
if (!entry.Enqueued)
{
entry.Enqueued = true;
queue.Add(entry);
}
}
}
public bool TryDequeue(out TValue value, TimeSpan timeout)
{
Entry entry;
// Try to dequeue an entry (with timeout).
if (!queue.TryTake(out entry, timeout))
{
value = default(TValue);
return false;
}
// Get exclusive access to the entry.
lock (entry)
{
// Return the value.
value = entry.Value;
// Mark the entry as dequeued.
entry.Enqueued = false;
entry.Value = default(TValue);
}
return true;
}
private class Entry
{
public TValue Value { get; set; }
public bool Enqueued { get; set; }
}
}
(这可能需要一两次代码审查,但我认为一般来说它是理智的。)
答案 2 :(得分:1)