在C#中实现阻塞队列

时间:2012-02-23 22:06:51

标签: c# multithreading queue

我使用以下代码来实现和测试阻塞队列。我通过启动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();
        }
    }
}

3 个答案:

答案 0 :(得分:17)

  

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

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

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

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

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

UPDATE:

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

嗯,你比我们更能回答这个问题,因为你是那个运行程序并查看输出的人。但就在我的脑海中,这是一种可能发生的方式:

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

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

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

答案 1 :(得分:2)

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

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

  • 消费者1正在等待
  • 制作人已放入一个项目并正在发出声响
  • 消费者1准备就绪
  • producer正在释放锁定
  • 消费者2刚刚输入删除,很幸运并获得锁定
  • 消费者2看到1项,不等待并取出项目
  • 使用者2发布锁定
  • 消费者1重新获取锁定但队列为空

这恰好就像documentation所说的那样:

  

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

答案 2 :(得分:1)

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

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

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

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

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

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