在C#中实现阻塞队列

我使用下面的代码来实现和测试阻塞队列。 我通过启动5个并发线程(删除程序)来测试队列,以便将项目从队列中拉出来,阻塞队列是否为空,以及1个并发线程(加法器)将项目间接添加到队列中。 但是,如果我让它运行的时间足够长,我会得到一个exception,因为即使队列为空,其中一个卸载线程也会退出等待状态。

有谁知道为什么我得到例外? 请注意,我很想知道为什么这不起作用而不是工作解决方案(因为我可以只谷歌)。

我非常感谢你的帮助。

using System; using System.Threading; using System.Collections.Generic; namespace Code { class Queue { private List q = new List(); public void Add(T item) { lock (q) { q.Add(item); if (q.Count == 1) { Monitor.Pulse(q); } } } public T Remove() { lock (q) { if (q.Count == 0) { Monitor.Wait(q); } T item = q[q.Count - 1]; q.RemoveAt(q.Count - 1); return item; } } } class Program { static Random r = new Random(); static Queue q = new Queue(); static int count = 1; static void Adder() { while (true) { Thread.Sleep(1000 * ((r.Next() % 5) + 1)); Console.WriteLine("Will try to add"); q.Add(count++); } } static void Remover() { while (true) { Thread.Sleep(1000 * ((r.Next() % 5) + 1)); Console.WriteLine("Will try to remove"); int item = q.Remove(); Console.WriteLine("Removed " + item); } } static void Main(string[] args) { Console.WriteLine("Test"); for (int i = 0; i < 5; i++) { Thread remover = new Thread(Remover); remover.Start(); } Thread adder = new Thread(Adder); adder.Start(); } } } 

如果我让它运行的时间足够长,我会得到一个exception,因为即使队列为空,其中一个卸载线程也会处于等待状态。 有谁知道为什么我得到例外?

问题很奇怪,因为很明显你知道答案:你的第一句话回答了第二句所提出的问题。 您得到exception,因为当队列为空时,卸载线程退出等待状态。

要解决这个问题,你需要使用循环而不是“if”。 正确的代码是:

 while(q.Count == 0) Monitor.Wait(q); 

 if(q.Count == 0) Monitor.Wait(q); 

更新:

一位意见提供者指出,或许你的问题是“在什么情况下消费者线程可以在队列为空时获得监视器?”

好吧,你处于比我们更好的位置,因为你是运行程序并查看输出的人。 但就在我的脑海中,这是一种可能发生的方式:

  • 消费者线程1:等待
  • 消费者线程2:准备好了
  • 生产者线程3:拥有监视器
  • 队列中有一个元素。
  • 线程3脉冲。
  • 线程1进入就绪状态。
  • 线程3放弃了监视器。
  • 线程2进入监视器。
  • 线程2使用队列中的项目
  • 线程2放弃了监视器。
  • 线程1进入监视器。

现在,线程1在监视器中有一个空队列。

一般来说,在推理这些问题时,你应该把“脉冲”看作是一只带有附注的鸽子。 一旦被释放,它与发送者没有任何联系,如果它找不到它的家,它就会在荒野中消失,其消息未送达。 你知道什么时候Pulse就是如果有任何线程在等待,那么一个线程将来会在某个时候进入就绪状态; 你不知道关于线程操作的相对时间的任何其他信息。

如果有1个消费者,那么你的代码会有效,但是当有更多消费者时,这个机制会失败并且应该是while(q.Count == 0) Monitor.Wait(q)

以下场景显示if(q.Count == 0) Monitor.Wait(q)何时失败(它与Eric的不同):

  • 消费者1正在等待
  • 制片人投入了一个项目并且正在发出声响
  • 消费者1准备好了
  • 制片人发布锁定
  • 消费者2刚进入删除,幸运并获得锁定
  • 消费者2看到1项,不等待并取出物品
  • 消费者2发布锁定
  • 消费者1重新获取锁定但队列为空

这恰好就像文档说它可能发生一样:

当调用Pulse的线程释放锁时,就绪队列中的下一个线程(不一定是脉冲线程)获取锁。

埃里克当然是对的; 事实是,虽然守则似乎涵盖了所有基础; 发生exception的事实表明你没有。

竞争条件是在Monitor.Wait上的Monitor.Pulse和加法器上的Monitor.Wait之间(释放锁定;但不一定立即触发等待唤醒并重新获取它的线程); 后续删除线程可以获取锁并立即跳转

 if (q.Count == 0) { Monitor.Wait(q); } 

声明并直接删除该项目。 然后, Pulse d线程唤醒并假设仍有一个项目; 但没有。

正如埃里克所说的那样,无论竞争条件实际表现如何,解决问题的方法都是如此。

同样,如果您阅读Monitor.Pulse上的示例,您将看到与您在此处所做的相似的设置,但这是一种微妙的不同方式。