C#中的队列和等待句柄

5

我在我的应用程序中使用以下代码已经有几年了,从来没有遇到过任何问题。

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、interlocked或lock等)。

使用Queue和AutoResetEvent的常用模式是什么,以解决这个问题并完成我尝试通过上述代码实现的操作?

(编辑后稍微更改了问题,因为正确指出Queue.Count可能会执行任何操作并且其实现是特定的。)


这个循环肯定嵌套在某个外部的 while(running) 类型的循环中 - 那不就是为什么它能正常工作的原因吗? - Ian Mercer
没有其他循环。只有一个线程锁定队列,将订单加入队列,然后设置NewOrderEvent。 - Michael Covelli
4个回答

4

在我看来,它看起来非常线程安全,WaitAny() 将立即完成,因为事件已经设置。这不是问题。

不要破坏已经运行良好的线程同步。但是,如果你想要更好的解决方案,可以考虑 Joe Duffy 在这篇杂志文章中介绍的 BlockingQueue。它的一个更通用的版本在 .NET 4.0 中提供,使用 ConcurrentQueue 作为其实际实现的System.Collections.Concurrent.BlockingCollection


但是由于这是一个AutoResetEvent,如果另一个线程在我还没有等待它的机会之前就设置了事件,那么我不会遇到问题吗?我同意,如果它是一个ManualResetEvent,那就是另外一回事了。 - Michael Covelli
哇...它是在4.0版本中?我怎么没收到备忘录呢?每天都有新的东西可以学习。 - Brian Gideon
@Michael:通常情况下,我不会对某个组件的内部工作做任何假设,或者基于那些不保证是静态的特性做出任何决策。如果文档说它不是线程安全的,那么我会同步所有访问(不仅仅是写访问)。你不一定会期望一个“字典”在一个线程添加值而另一个线程查找不同值时抛出异常,但有时确实会这样。如果你能在这里使用双重检查锁定,那么你很幸运。 - Aaronaught
当您以线程不安全的方式使用线程不安全对象的属性或方法时,除了死亡和税收之外,没有什么是保证的。但是Count会有可预测的错误行为:不会改变对象状态的属性不会破坏它。它只能返回错误的值。Duffy不喜欢冒险,这是无法争辩的。我会争论改变已经运作良好数年的东西。 - Hans Passant
@Hans 感谢你在思考这个问题时的帮助。大家的共识似乎是,依赖可能会改变的实现细节是一个不好的想法,即使这段代码目前能够工作。我想我应该依赖你给我的链接来重新处理这个问题。谢谢! - Michael Covelli
显示剩余10条评论

3
您是正确的,这段代码不是线程安全的。但原因不是您想的那样。
AutoResetEvent 没有问题。尽管如此,只是因为您获取了锁并重新测试了 PendingOrders.Count。事实上,问题的关键在于您在锁外调用了 PendingOrders.Count。由于 Queue 类不是线程安全的,您的代码也就不是线程安全的......就这样。
现实中,您可能永远不会遇到这个问题,有两个原因。首先,Queue.Count 属性几乎肯定被设计为永远不会使对象处于半成品状态。毕竟,它可能只会返回一个实例变量。其次,在该读取上缺少内存屏障对您代码的更广泛的上下文没有重大影响。最糟糕的情况是您将在循环的一次迭代中得到过期的读取,然后获取的锁将隐式创建一个内存屏障,并在下一次迭代中进行新鲜读取。我在这里假设只有一个线程在排队项目。如果有两个或更多线程,则情况会发生很大变化。
然而,让我明确一点。您无法保证 PendingOrders.Count 在执行期间不会改变对象的状态。因为它没有包含在锁中,另一个线程可以在它仍处于半成品状态时启动对其的操作。

事实上,Queue.Count 在当前实现中似乎只返回一个 int 字段,并且没有标记为 volatile。 - Ian Mercer
嗨,布赖恩,关于你的第一个观点,我同意。任意的Queue<T>实现都可以在Count属性中执行任何操作,包括遍历所有元素。我应该修改问题,以便我们可以假设Queue<T>.Count只返回一个实例变量(非易失性、非交错读取)。 - Michael Covelli
我同意可能会得到一个陈旧的读取。也就是说,Count 的实际值为1,但在 while 循环条件中读取 Count 返回0。但我认为这仍然可以接受。Count > 0 检查只是为了捕捉生产者同时将 M 个订单排队的情况,以便我不仅处理其中一个订单。我认为你是对的 - 锁的内存屏障会拯救我。只是试图思考是否存在任何实际情况会出现问题。(包括有 N 个生产者线程的情况,假设每个线程在入队之前都锁定了队列)。 - Michael Covelli
@Michael:是的,你很有可能是对的。实际上,你可能没有问题。这非常难以想象。即使是线程专家也会承认这是一个极其困难的问题。你最好的选择是获取一个正确的BlockingQueue实现并使用它。如果你正在使用新版本的.NET框架,那么它自带了一个。 - Brian Gideon

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

这看起来似乎可以工作。让我再考虑一下,但是在我看来那是正确的。谢谢! - Michael Covelli
我应该澄清,这只有在单个线程执行入队操作时才是安全的。如果有两个或更多线程执行入队操作,则可能会出现队列有一个项目但 ARE 没有设置的情况。 - Brian Gideon
1
@Michael:因为两个进队线程在单个出队线程被释放之前可能会调用ARE上的Set方法。这将使队列中有2个项目,但在ARE被重置之前只有一个出队线程被释放。这意味着只有1个项目被出队。 - Brian Gideon
@Brian 哦,我明白你的意思了。如果没有 Count > 0 这种检查方式,那么这段代码确实存在问题。如果两个线程同时排队,我们只会得到第一个项目。 - Michael Covelli
@csharptest.net 您说得对,我没有仔细阅读您的代码。我认为这对于任何数量的读取者来说都是线程安全的。 - Michael Covelli
显示剩余5条评论

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
}

如果在执行一个项目时有两个项目排队,那么只会出队其中一个,然后等待另一个被排队。 - Ian Mercer
这正是导致我首先使用 Count > 0 检查的问题所在。如果另一个线程在我进入 while 循环时排队并设置了事件,那么我将会错过它。 - Michael Covelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接