【问题标题】:Multithreaded .NET queue problems多线程 .NET 队列问题
【发布时间】:2011-09-17 20:39:54
【问题描述】:

我的代码中有一个奇怪的错误。这是极其罕见的(可能每隔几周发生一次),但它就在那里,我不知道为什么。

我们有 2 个线程在运行,1 个线程获取网络消息并将它们添加到队列中,如下所示:

DataMessages.Enqueue(new DataMessage(client, msg));

另一个线程从这个队列中取出消息并处理它们,如下所示:

while (NetworkingClient.DataMessages.Count > 0)
{
    DataMessage message = NetworkingClient.DataMessages.Dequeue();

    switch (message.messageType)
    {
       ...
    }
}

但是,我经常在 switch (message.messageType) 行上收到 NullReferenceException,我可以在调试器中看到该消息为空。

不可能将空值放入队列(参见代码的第一部分),这是唯一使用队列的两件事。

队列不是线程安全的吗,是不是我在另一个线程入队的确切时刻出队,这导致了故障?

【问题讨论】:

  • .NET BCL 中很少有东西以这种方式是线程安全的。在这种情况下,“为了保证 Queue 的线程安全,所有操作都必须通过Synchronized 方法返回的包装器来完成”。如果适用,您的问题也可能是两个线程同时入队或出队。无论哪种方式,线程安全都是您的责任。
  • +1,好问题,它确实展示了竞争条件及其结果(损坏/不稳定/意外情况)。
  • 从您的代码看来,您似乎只是在为您的线程做一个简单的循环。您应该考虑实施适当的bounded-buffer 来同步它。

标签: c# .net multithreading queue


【解决方案1】:

队列不是线程安全的吗? 我正在出列的确切时刻 另一个线程正在排队并且 这会导致故障吗?

没错。 Queue 不是线程安全的。线程安全队列是System.Collections.Concurrent.ConcurrentQueue。改用它来解决您的问题。

【讨论】:

  • 不要盲目使用ConcurrentQueue;仅当您知道自己在做什么时才使用它。在某些情况下,带锁的Queue 是更好的选择。并发集合类不是并发问题的灵丹妙药:)
【解决方案2】:
    while (NetworkingClient.DataMessages.Count > 0)
    {
        // once every two weeks a context switch happens to be here.
        DataMessage message = NetworkingClient.DataMessages.Dequeue();

        switch (message.messageType)
        {
           ...
        }
    }

...当您在该位置获得上下文切换时,第一个表达式的结果 (NetworkingClient.DataMessages.Count > 0) 对两个线程都是正确的,并且到达Dequeue() 操作的第一个是对象,第二个线程得到的是 null(而不是 InvalidOperationException,因为队列的内部状态没有完全更新以抛出正确的例外)。

现在你有两个选择:

  1. 使用 .NET 4.0 ConcurrentQueue

  2. 重构您的代码:

让它看起来像这样:

while(true)
{
  DataMessage message = null;

  lock(NetworkingClient.DataMessages.SyncRoot) {
       if(NetworkingClient.DataMessages.Count > 0) {
          message = NetworkingClient.DataMessages.Dequeue();
       } else {
         break;
       }
    }
    // .. rest of your code
}

编辑:更新以反映 Heandel 的评论。

【讨论】:

  • 问题确实说“另一个线程从这个队列中取出消息并处理它们”。这意味着永远不会发生来自 2 个线程的同时出队。 #justsaying
【解决方案3】:

如果您对确切原因感兴趣:

Enqueue 看起来像这样:

this._array[this._tail] = item;
this._tail = (this._tail + 1) % this._array.Length;
this._size++;
this._version++;

像这样Dequeue

T result = this._array[this._head];
this._array[this._head] = default(T);
this._head = (this._head + 1) % this._array.Length;
this._size--;
this._version++;

比赛是这样进行的:

  • 队列中有 1 个元素(头 == 尾),因此您的阅读器线程开始出列,但在 Dequeue 的第一行之后被中断
  • 然后将另一个元素排入队列并放在位置tail,此时等于head
  • 现在Dequeue 恢复并用default(T) 覆盖刚刚由Enqueue 插入的元素
  • 下次调用 dequeue 时,您将获得默认值 (T)(在您的情况下为 null)而不是实际值

【讨论】:

  • +1,很高兴看到幕后发生的一切。我想我在多线程时应该更加小心!我很高兴我发现了这个错误,但它不会发布到版本中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-07
  • 1970-01-01
  • 2013-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多