【问题标题】:Threading problem with Monitor.Wait() and Monitor.Pulse()Monitor.Wait() 和 Monitor.Pulse() 的线程问题
【发布时间】:2010-10-18 01:43:38
【问题描述】:

我在 ASP.NET 中有一个生产者-消费者场景。我设计了一个Producer 类,一个Consumer 类和一个用于保存共享对象并负责生产者和消费者之间通信的类,我们称之为Mediator。因为我在启动时(在父对象中)分叉了执行路径,一个线程会调用Producer.Start(),而另一个线程会调用Consumer.Start(),所以我需要将Mediator 的引用传递给ProducerConsumer (通过Constructor)。 Mediator 是一个智能类,它将优化许多事情,比如它的内部队列的长度,但现在将其视为循环阻塞队列。 Producer 会将新对象入队Mediator,直到队列已满,然后Producer 将阻塞。 Consumer dequeues 对象从 Mediator 直到队列中没有任何内容。对于线程之间的信令,我在Mediator 类中实现了两个方法:Wait()Pulse()。代码是这样的:

Class Mediator
{
  private object _locker = new object();

  public void Wait()
  {
    lock(_locker)
      Monitor.Wait(_locker);
  }

  public void Pulse()
  {
    lock(_locker)
      Monitor.Pulse(_locker);
  }
}

// This way threads are signaling:

Class Consumer
{
  object x;
  if (Mediator.TryDequeue(out x))
    // Do something
  else
    Mediator.Wait();
}

在 Mediator 内部,我每次 EnqueuedDequeued 使用 this.Pulse(),这样等待的线程就会收到信号并继续工作。

但我遇到了死锁,因为我从未使用过这种设计来发送信号线程,所以我不确定设计有问题还是我在其他地方做错了什么?

谢谢

【问题讨论】:

  • 为什么每次出队时都要调用 Pulse(从而让生产者重新获得锁)?通常消费者会消费直到没有剩余物品,然后才调用Wait而不是Pulse。
  • 因为生产者正在从外部服务器接收数据,我不确定它们的速度和可用性。所以对于 QoS(服务质量),我提前缓冲了 5 分钟消费者的需求。

标签: c# asp.net multithreading


【解决方案1】:

这里没有太多代码可以继续,但我最好的猜测是你有一个live-lock 问题。如果在Mediator.Wait 之前调用Mediator.Pulse,那么即使队列中有东西,信号也会丢失。这是实现阻塞队列的标准模式。

public class BlockingQueue<T>
{
  private Queue<T> m_Queue = new Queue<T>();

  public void Enqueue(T item)
  {
    lock (m_Queue)
    {
      m_Queue.Enqueue(item);
      Monitor.Pulse(m_Queue);
    }
  }

  public T Dequeue()
  {
    lock (m_Queue)
    {
      while (m_Queue.Count == 0) 
      {
        Monitor.Wait(m_Queue);
      }
      return m_Queue.Dequeue();
    }
  }
}

注意Monitor.Wait 仅在队列为空时调用。还要注意它是如何在while 循环中调用的。这是因为Wait 的优先级不高于Enter,因此进入Dequeue 的新线程可能会占用最后一项,即使对Wait 的调用已准备好返回。如果没有循环,线程可能会尝试从空队列中删除项目。

【讨论】:

    【解决方案2】:

    如果您可以使用 .NET 4,最好的办法是使用 BlockingCollection&lt;T&gt; (http://msdn.microsoft.com/en-us/library/dd267312.aspx) 来处理队列长度。

    【讨论】:

    • 我简化了场景。 Mediator 是一个复杂的类,可以做更多的事情。该项目已经在 .NET 4.0 上,并且已经使用了 ConcurrentQueue 和 ConcurrentBag 等一些功能。问题是关于生产者和消费者线程之间的信令方法。
    • BlockingCollection&lt;T&gt; 几乎可以肯定是他们之间交流的最佳方式。比尝试编写自己的信号、排队和出队代码要好得多,除非您有非常具体的要求无法使用它来满足。
    • Monitor.Wait 在调用时释放锁,如果线程被选择接收Monitor.Pulse 信号,则重新获取它。
    【解决方案3】:

    设计没有错。

    当您使用Monitor.Wait()Monitor.Pulse() 当您不知道哪个线程将首先完成它的工作(生产者或消费者)时,问题就会出现。在这种情况下,使用AutoResetEvent 可以解决问题。当消费者到达应该消费生产者生成的数据的部分时,请考虑消费者。也许它在生产者发出脉冲之前到达那里,那么一切都很好,但是如果消费者在生产者发出信号后到达那里怎么办。是的,然后您会遇到死锁,因为生产者已经为该部分调用了 Monitor.Pulse() 并且不会重复它。 使用AutoResetEvent,您确定消费者在那里等待来自生产者的信号,如果生产者在消费者到达该部分之前已经发出信号,则大门打开,消费者将继续。

    可以在 Mediator 中使用 Monitor.Wait()Monitor.Pulse() 来发送等待线程的信号。

    【讨论】:

      【解决方案4】:

      是否有可能因为 Pulse 不存储任何状态而发生死锁?这意味着如果ProducerConsumer 调用Wait 之前/之后调用Pulse,那么Wait 将阻塞。这是documentationMonitor.Pulse 的注释

      另外,您应该知道object x = new object(); 是无关的——out 调用将初始化x,因此创建的对象将超出TryDequeue 调用的范围。

      【讨论】:

      • 感谢您的注意,因为我没有在 x 上使用 new。代码编辑为正确版本 -> 对象 x;
      • 也许在这种情况下使用“AutoResetEvent”会更好,但它的性能是否足够好。代码应该高度优化。
      • 我认为 AutoResetEvent 在性能方面已经足够好了,这比在TryDequeue 成功之前让线程休眠要好...
      【解决方案5】:

      很难用提供的代码示例来判断。

      • 锁是否在其他地方?在调解员内?
      • 线程是否只是在获取锁而不是在实际的等待调用时停止?
      • 您是否暂停了调试器中的线程以查看当前状态?
      • 您是否尝试过一个简单的测试,只需将一个简单的单个值放入队列并使其工作?还是 Mediator 在这一点上相当复杂?

      在 Mediator 类和您的生产者类中提供更多详细信息之前,这是一些疯狂的猜测。似乎某些线程可能在您不期望的情况下持有锁。一旦脉冲,您确实需要通过退出“锁定”范围来释放任何线程可能拥有的锁。所以,如果在 Mediator 的某个地方你有锁,然后调用 Pulse,你需要退出持有锁的最外层范围,而不仅仅是 Pulse 中的那个。

      【讨论】:

      • 'Producer' 和 'Consumer' Start() 方法都进入了无限循环。当他们需要相互发送信号时,他们使用 Mediator 上的方法。在调试器上一切都很好,似乎至少有一个线程在错误的时间到达了某个地方。在这种情况下使用“AutoResetEvent”是否更好,它的性能是否足够好?
      • 我不得不认为根本问题仍然没有解决。虽然 AutoResetEvent 可能暂时让事情重新开始,但仍然存在一些潜在的问题。设置本身的性能可能还可以,但是您的无限循环可能会导致许多不必要的获取和释放锁。有了您对程序功能的所有说明,Monitor.Wait() 和 Monitor.Pulse() 就足够了。 davisoa 指出的 MS 文档有一个 Producer/Consumer 示例,看起来很简单,可以使用。
      • 来自 MSDN 的那篇文章 (msdn.microsoft.com/en-us/library/yy12yx1f%28VS.80%29.aspx) 不起作用,您可以在文章底部找到来自 Brian Gideon 的最新评论
      • 不确定该链接来自何处。我指的是 davisoa (msdn.microsoft.com/en-us/library/…) 列出的那个。
      【解决方案6】:

      你能重构一个普通的消费者/生产者队列吗?然后可以在单个类中处理入队和出队以及线程信号,因此无需传递公共锁。然后可以通过委托处理出队过程。如果你愿意,我可以发布一个例子。

      【讨论】:

      • 对象很复杂,将它们全部放在一个类中会使该类难以理解和维护。
      猜你喜欢
      • 1970-01-01
      • 2021-12-23
      • 2013-11-14
      • 1970-01-01
      • 2021-12-10
      • 2014-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多