我的应用程序中已经有以下代码多年,并且从未见过它的问题。
while ((PendingOrders.Count > 0) || (WaitHandle.WaitAny(CommandEventArr) != 1))
{
lock (PendingOrders)
{
if (PendingOrders.Count > 0)
{
fbo = PendingOrders.Dequeue();
}
else
{
fbo = null;
}
}
// Do Some Work if fbo is != null
}
其中CommandEventArr由NewOrderEvent(自动重置事件)和ExitEvent(手动重置事件)组成。
但是我不确定这是否是线程安全的(假设N个生成器线程在排队之前全部锁定队列,并且一个消费者线程运行上面的代码)。此外,我们可以假设Queue.Count属性只从Queue类返回一个实例Int32值(没有volatile或者互锁或锁定等)。
Queue和AutoResetEvent用于修复此问题的常用模式是什么,并且我正在尝试使用上面的代码做什么?
(编辑后,在正确指出Queue.Count可以执行任何操作及其特定实现之后稍微更改问题。)
答案 0 :(得分:4)
对我来说看起来非常线程安全,WaitAny()将立即完成,因为已经设置了事件。那是不问题。
不要破坏有效的线程同步。但是如果你想要一个更好的捕鼠器,你可以在这个magazine article中考虑Joe Duffy的BlockingQueue。它的更通用版本在.NET 4.0中可用,System.Collections.Concurrent.BlockingCollection使用ConcurrentQueue作为它的实际实现。
答案 1 :(得分:3)
你是对的。代码不是线程安全的。但不是你想的原因。
AutoResetEvent很好。虽然,只是因为你获得了一个锁并重新测试PendingOrders.Count。问题的真正症结在于您在锁定之外调用PendingOrders.Count。因为Queue类不是线程安全的,所以你的代码不是线程安全的...句号。
现在实际上你可能永远不会有这个问题有两个原因。首先,Queue.Count属性几乎肯定是为了永远不会让对象处于半生不熟的状态。毕竟,它可能只返回一个实例变量。其次,在读取时缺少内存屏障不会对代码的更广泛上下文产生重大影响。最糟糕的事情是你将在循环的一次迭代中得到一个陈旧的读取,然后获得的锁将隐式地创建一个内存屏障,并在下一次迭代时发生新的读取。我在这里假设只有一个线程排队项目。如果有2个或更多,情况会发生很大变化。
但是,让我非常清楚。 您无法保证PendingOrders.Count在执行期间不会改变对象的状态。并且因为它没有包含在锁中,所以另一个线程可以在它处于半支持状态时启动它的操作。
答案 2 :(得分:2)
使用手动活动......
ManualResetEvent[] CommandEventArr = new ManualResetEvent[] { NewOrderEvent, ExitEvent };
while ((WaitHandle.WaitAny(CommandEventArr) != 1))
{
lock (PendingOrders)
{
if (PendingOrders.Count > 0)
{
fbo = PendingOrders.Dequeue();
}
else
{
fbo = null;
NewOrderEvent.Reset();
}
}
}
然后你需要确保在入队方面锁定:
lock (PendingOrders)
{
PendingOrders.Enqueue(obj);
NewOrderEvent.Set();
}
答案 3 :(得分:0)
您应该只使用WaitAny,并确保在添加到PendingOrders集合的每个新订单上发出信号:
while (WaitHandle.WaitAny(CommandEventArr) != 1))
{
lock (PendingOrders)
{
if (PendingOrders.Count > 0)
{
fbo = PendingOrders.Dequeue();
}
else
{
fbo = null;
//Only if you want to exit when there are no more PendingOrders
return;
}
}
// Do Some Work if fbo is != null
}