我使用以下代码来实现和测试阻塞队列。我通过启动5个并发线程(删除程序)来测试队列,以便将项目从队列中拉出来,阻塞队列是否为空,以及1个并发线程(加法器)将项目间接添加到队列中。但是,如果我让它运行的时间足够长,我会得到一个异常,因为即使队列为空,其中一个卸载线程也会处于等待状态。
有谁知道我为什么会得到这个例外?请注意,我很想知道为什么这不起作用而不是工作解决方案(因为我可以只谷歌)。
我非常感谢你的帮助。
using System;
using System.Threading;
using System.Collections.Generic;
namespace Code
{
class Queue<T>
{
private List<T> q = new List<T>();
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<int> q = new Queue<int>();
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();
}
}
}
答案 0 :(得分:17)
如果我让它运行的时间足够长,我会得到一个异常,因为即使队列为空,其中一个卸载线程也会处于等待状态。有谁知道为什么我得到例外?
问题很奇怪,因为很明显你知道答案:你的第一句话回答了第二句所提出的问题。 您得到异常,因为当队列为空时,移除线程退出等待状态。
要解决此问题,您需要使用循环而不是“if”。正确的代码是:
while(q.Count == 0) Monitor.Wait(q);
不
if(q.Count == 0) Monitor.Wait(q);
一位意见提供者指出,或许你的问题是“当队列为空时,消费者线程在什么情况下可以获得监视器?”
嗯,你比我们更能回答这个问题,因为你是那个运行程序并查看输出的人。但就在我的脑海中,这是一种可能发生的方式:
现在,线程1在监视器中有一个空队列。
一般来说,在对这些问题进行推理时,你应该把“脉冲”看作是一只带有附注的鸽子。一旦被释放,它与发送者没有任何联系,如果它找不到它的家,它就会在荒野中死去,而它的消息没有传递。所有你知道当你的脉冲是如果有任何线程等待,那么一个线程将在未来的某个时间移动到就绪状态;你不知道关于线程操作的相对时间的其他任何事情。
答案 1 :(得分:2)
如果有1个消费者,你的代码会有效,但是当有更多消费者时,这个机制会失败并且它应该是while(q.Count == 0) Monitor.Wait(q)
以下方案显示if(q.Count == 0) Monitor.Wait(q)
何时失败(与Eric的不同):
这恰好就像documentation所说的那样:
当调用Pulse的线程释放锁定时,就绪队列中的下一个线程(不一定是脉冲线程)获取锁定。
答案 2 :(得分:1)
竞争条件是去除器上的Monitor.Wait
和加法器上的Monitor.Pulse
之间(释放锁定;但不一定会立即触发线程等待唤醒并重新获取它) );后续删除线程可以获取锁并立即跳转
if (q.Count == 0)
{
Monitor.Wait(q);
}
声明并直接删除该项目。然后,Pulse
d线程醒来并假设还有一个项目;但没有。
正如埃里克所说的那样,解决它的方式,无论竞争条件实际表现如何,都是如此。
同样,如果您阅读Monitor.Pulse
上的示例,您会看到与此处所做的相似的设置,但这是一种完全不同的方式。