延迟生产者消费者模式

时间:2014-05-01 14:51:03

标签: c# .net-3.5

我有一个生产者通过爆发产生整数(在几秒钟内产生1到50)。我有一个消费者按块使用这些整数。

我希望消费者在制作人完成他的爆发后开始消费(我没有在制作人的领导上,我只知道它已经完成了生产,当没有产生任何东西5秒)。

我想到了两种不同的方式:

首先:使用一个不同于另一个的消费者:

private readonly List<int> _ids = new List<int>();
private readonly ManualResetEvent _mainWaiter = new ManualResetEvent(false);
private readonly ManualResetEvent _secondaryWaiter = new ManualResetEvent(false);

//This methods consumes the id from the producer
public void OnConsumeId(int newId)
{
    lock(_ids)
    {
        _ids.Add(newId);
        _mainWaiter.Set();
        _secondaryWaiter.Set();
    }
}

//This methods runs on the dedicated thread :
public void ConsumerIdByBlock()
{
    while(true)
    {
        _mainWaiter.Wait();
        while(_secondaryWaiter.Wait(5000));

        List<int> localIds;
        lock(_ids)
        {
            localIds = new List<int>(_ids);
            _ids.Clear();
        }
        //Do the job with localIds
    }
}

第二:为最后一次更新提供一种令牌

//This methods consumes the id from the producer
private int _lastToken;
public void OnConsumeId(int newId)
{
    lock(_ids)
    {
        _ids.Add(newId);
        ThreadPool.Queue(()=>ConsumerIdByBlock(++_lastToken));
    }
}

//This methods runs on the dedicated thread :
public void ConsumerIdByBlock(int myToken)
{       
    Thread.Sleep(5000);

    List<int> localIds;
    lock(_ids)
    {
        if(myToken !=_lastToken)
            return;     

        localIds = new List<int>(_ids);
        _ids.Clear();
    }

    //Do the job with localIds  
}

但我发现这些方法对于这样做有点过于复杂。是否存在原生/更简单的解决方案?你会怎么做?

4 个答案:

答案 0 :(得分:7)

如果您使用已经具有通知等的线程安全队列,这将变得更加容易。 BlockingCollection使编写制作人 - 消费者的东西变得非常容易。

我喜欢你的“链接消费者”的想法,因为你不必修改生产者才能使用它。也就是说,生产者只是把东西塞进队列中。消费者最终如何使用它是无关紧要的。然后,使用BlockingCollection,您将拥有:

BlockingCollection<ItemType> inputQueue = new BlockingCollection<ItemType>();
BlockingCollection<List<ItemType>> intermediateQueue = new BlockingCollection<List<ItemType>>();

您的制作人通过调用inputQueue.Add将内容添加到输入队列中。您的中间消费者(称之为整合者)通过超时调用TryTake从队列中获取内容。例如:

List<ItemType> items = new List<ItemType>();
while (!inputQueue.IsCompleted)
{
    ItemType t;
    while (inputQueue.TryTake(out t, TimeSpan.FromSeconds(5))
    {
        items.Add(t);
    }
    if (items.Count > 0)
    {
        // Add this list of items to the intermediate queue
        intermediateQueue.Add(items);
        items = new List<ItemType>();
    }
}

第二个消费者只读取中间队列:

foreach (var itemsList in intermediateQueue.GetConsumingEnumerable))
{
    // do something with the items list
}

无需ManualResetEventlock或其中任何一项; BlockingCollection为你处理所有混乱的并发内容。

答案 1 :(得分:1)

为了扩展@Chris的想法,当你使用id时,记住它的时间。如果自上一个过去超过5秒,则启动新列表设置事件。您的块消费者只需等待该事件并使用存储的列表。

另请注意,在第一个解决方案中,ConsumerIdByBlock可能在OnConsumeId获取锁定之前退出内部,然后ConsumerIdByBlock将消耗至少一个Id太多。

答案 2 :(得分:1)

队列似乎是处理您所描述内容的最佳方式。

不幸的是,

线程安全的集合有点用词不当。有&#34;阻止&#34; .NET中的集合,但新类的基本原则是不要尝试创建基于实例的类&#34;线程安全&#34; (静态课程是一个不同的故事。课堂级别&#34;线程安全&#34;是一个为所有人提供的一切主题。而且这个公理硬币的第二面是“没有人”#34;它无法针对特定应用程序的需求或用途进行优化,因此它最终会采用最坏情况,并且可能无法解决所有应用程序的所有情况错过的事情最终需要通过另一种手段来解决,这两种方式的相互作用需要独立管理。

队列是一种基本的读取器/写入器模式,可以使用称为读写器锁的锁定原语进行封装。其中有一个名为ReaderWriterLockSlim的类,可用于确保使用队列集的应用程序级线程安全性。

答案 3 :(得分:1)

我会使用System.Timers.Timer。设置5000ms间隔,每次产生新ID时,重新启动Timer:

   class Consumer
   {
      List<int> _ids = new List<int>();
      Timer producer_timer = new Timer();

      public Consumer()
      {
         producer_timer.Elapsed += ProducerStopped;
         producer_timer.AutoReset = false;
      }

      public void OnConsumeId(int newId)
      {
         lock (_ids)
         {
            _ids.Add(newId);
            producer_timer.Interval = 5000;
            producer_timer.Start();
         }
      }

      public void ProducerStopped(object o, ElapsedEventArgs e)
      {
         // Do job here.
      }
   }