如何实现自己的高级生产者/消费者方案?

时间:2010-11-10 10:14:16

标签: c# multithreading .net-3.5 queue


注意:
我对我的问题进行了彻底的修改。您可以通过更改历史记录查看原始问题。


我需要一个“强大”队列,它提供以下功能:

  • 我有一组对象的特定范围。这意味着 A组 B组,......将拥有自己的队列
  • 我在一个组范围的线程中填充队列线程A (生产者)
  • 我在团队范围的线程中读取队列线程B (消费者)

所以我将有以下场景:

  1. 队列中没有项目(因为作业是使用空的“targetgroup”调用的):线程B 应该逃避循环
  2. 目前队列中没有任何项目,因为主题A 正在处理要排队的项目:主题B 应该等待
  3. 队列中有项目:主题B 应该能够出列并处理项目
  4. 队列中没有项目,因为主题A 没有更多要排队的项目:主题B 应该逃脱循环
  5. 现在我提出了以下实施方法:

    public class MightyQueue<T>
      where T : class
    {
        private readonly Queue<T> _queue = new Queue<T>();
    
        private bool? _runable;
        private volatile bool _completed;
    
        public bool Runable
        {
            get
            {
                while (!this._runable.HasValue)
                {
                    Thread.Sleep(100);
                }
                return this._runable ?? false;
            }
            set
            {
                this._runable = value;
            }
        }
    
        public void Enqueue(T item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
    
            this._queue.Enqueue(item);
        }
    
        public void CompleteAdding()
        {
            this._completed = true;
        }
    
        public bool TryDequeue(out T item)
        {
            if (!this.Runable)
            {
                item = null;
                return false;
            }
            while (this._queue.Count == 0)
            {
                if (this._completed)
                {
                    item = null;
                    return false;
                }
                Thread.Sleep(100);
            }
            item = this._queue.Dequeue();
            return true;
        }
    }
    
    然后将使用

    生产者

    if (anythingToWorkOn)
    {
        myFooMightyQueueInstance.Runable = false;
    }
    else
    {
        myFooMightyQueueInstance.Runable = true;
        while (condition)
        {
            myFooMightyQueueInstance.Enqueue(item);
        }
        myFooMightyQueueInstance.CompleteAdding();
    }
    

    消费

    if (!myFooMightyQueueInstance.Runable)
    {
        return;
    }
    
    T item;
    while (myFooMightyQueueInstance.TryDequeue(out item))
    {
        //work with item
    }
    

    但是我相信,这种方法是错误的,因为我在那里使用了一些Thread.Sleep() - 东西(不能有一些waitHandle或其他东西?)...我不是关于算法本身要么...... 有人可以帮帮我吗?

5 个答案:

答案 0 :(得分:2)

如果您有.Net 4.0,请使用BlockingCollection。它通过CompleteAdding方法处理所有混乱,包括最后一点。

如果您有早期的.Net,那么升级(即,我懒得解释如何实施已经为您完成的事情。)

编辑:我认为您的问题根本不需要线程。只需提前创建所有电子邮件,然后再睡到指定的时间。

答案 1 :(得分:1)

您可以使用conditionvariables完成所需的操作。我将编写一个伪代码示例,不应该太难实现。

一个主题有以下几点:

while(run)
  compose message
  conditionvariable.lock()
  add message to queue
  conditionvariable.notifyOne()
  conditionvariable.release()

虽然另一个主题有这些内容

while(threadsafe_do_run())
  while threadsafe_queue_empty()
       conditionvariable.wait()
  msg = queue.pop()
  if msg == "die"
      set_run(false)
  conditionvariable.release()
  send msg

因此,如果您没有收到任何消息,请发送消息。处理完所有消息后也是如此。

do_run()和queue_empty()应该线程安全地检查他们的东西,使用适当的锁。

wait()在调用notifyOne()时返回,然后队列有一个要发送的消息。在大多数框架中,conditionvariable已经有了锁,你可能需要在.NET中自己添加lock语句。

答案 2 :(得分:1)

您应该从通用的Producer-Consumer队列开始并使用它。在队列中实现这个并不是一个好主意,因为这会阻止您使用信号量来发送线程信号(或者,您可以在队列中使用公共信号量,但这是一个真的坏主意)。 / p>

一旦线程A将单个工作项排入队列,它就必须发出信号通知线程B.当线程B处理完所有项目后,它应发出信号通知信号通知其他人已完成。你的主线程应该等待第二个信号量知道一切都已完成。

<强> [编辑]

首先,你有一个制作人和一个消费者:

public interface IProducer<T> : IStoppable
{
    /// <summary>
    /// Notifies clients when a new item is produced.
    /// </summary>
    event EventHandler<ProducedItemEventArgs<T>> ItemProduced;
}

public interface IConsumer<T> : IStoppable
{
    /// <summary>
    /// Performs processing of the specified item.
    /// </summary>
    /// <param name="item">The item.</param>
    void ConsumeItem(T item);
}

public interface IStoppable
{
    void Stop();
}

因此,在您的情况下,创建邮件的类需要触发ItemProduced事件,发送它的类需要实现ConsumeItem

然后将这两个实例传递给Worker的实例:

public class Worker<T>
{
    private readonly Object _lock = new Object();
    private readonly Queue<T> _queuedItems = new Queue<T>();
    private readonly AutoResetEvent _itemReadyEvt = new AutoResetEvent(false);
    private readonly IProducer<T> _producer;
    private readonly IConsumer<T> _consumer;
    private volatile bool _ending = false;
    private Thread _workerThread;

    public Worker(IProducer<T> producer, IConsumer<T> consumer)
    {
        _producer = producer;
        _consumer = consumer;
    }

    public void Start(ThreadPriority priority)
    {
        _producer.ItemProduced += Producer_ItemProduced;
        _ending = false;

        // start a new thread
        _workerThread = new Thread(new ThreadStart(WorkerLoop));
        _workerThread.IsBackground = true;
        _workerThread.Priority = priority;
        _workerThread.Start();
    } 

    public void Stop()
    {
        _producer.ItemProduced -= Producer_ItemProduced;
        _ending = true;

        // signal the consumer, in case it is idle
        _itemReadyEvt.Set();
        _workerThread.Join();
    }

    private void Producer_ItemProduced
         (object sender, ProducedItemEventArgs<T> e)
    {
        lock (_lock) { _queuedItems.Enqueue(e.Item); }

        // notify consumer thread
        _itemReadyEvt.Set();
    }

    private void WorkerLoop()
    {
        while (!_ending)
        {
            _itemReadyEvt.WaitOne(-1, false);

            T singleItem = default(T);
            lock (_lock)
            {
               if (_queuedItems.Count > 0)
               {
                   singleItem = _queuedItems.Dequeue();
               }
            }


            while (singleItem != null)
            {
                try
                {
                    _consumer.ConsumeItem(singleItem);
                }
                catch (Exception ex)
                {
                    // handle exception, fire an event
                    // or something. Otherwise this
                    // worker thread will die and you
                    // will have no idea what happened
                }

                lock (_lock)
                {
                    if (_queuedItems.Count > 0)
                    {
                        singleItem = _queuedItems.Dequeue();
                    }
                }
            }

         }

    } // WorkerLoop

} // Worker

这是一般的想法,可能需要进行一些额外的调整。

要使用它,您需要让您的类实现这两个接口:

IProducer<IMail> mailCreator = new MailCreator();
IConsumer<IMail> mailSender = new MailSender();

Worker<IMail> worker = new Worker<IMail>(mailCreator, mailSender);
worker.Start();

// produce an item - worker will add it to the
// queue and signal the background thread
mailCreator.CreateSomeMail();

// following line will block this (calling) thread
// until all items are consumed
worker.Stop();

关于这一点的好处是:

  • 你可以拥有自己喜欢的工人
  • 多名工人可以接受来自同一制片人的作品
  • 多个工作人员可以将项目分派给同一个消费者(虽然这意味着您需要采用以线程安全的方式实现消费者的案例)。

答案 3 :(得分:1)

我写了一个简单的例子,对我来说很好,应该适合你的场景。如果消费者正在运行取决于运行变量的设置方式,但您可以轻松地将其修改为更复杂的条件,例如“如果没有邮件但有人说我应该等待更多”。

public class MailSystem
{
    private readonly Queue<Mail> mailQueue = new Queue<Mail>();
    private bool running;
    private Thread consumerThread;

    public static void Main(string[] args)
    {
        MailSystem mailSystem = new MailSystem();
        mailSystem.StartSystem();
    }

    public void StartSystem()
    {
        // init consumer
        running = true;
        consumerThread = new Thread(ProcessMails);
        consumerThread.Start();
        // add some mails
        mailQueue.Enqueue(new Mail("Mail 1"));
        mailQueue.Enqueue(new Mail("Mail 2"));
        mailQueue.Enqueue(new Mail("Mail 3"));
        mailQueue.Enqueue(new Mail("Mail 4"));
        Console.WriteLine("producer finished, hit enter to stop consumer");
        // wait for user interaction
        Console.ReadLine();
        // exit the consumer
        running = false;
        Console.WriteLine("exited");
    }

    private void ProcessMails()
    {
        while (running)
        {
            if (mailQueue.Count > 0)
            {
                Mail mail = mailQueue.Dequeue();
                Console.WriteLine(mail.Text);
                Thread.Sleep(2000);
            }
        }
    }
}

internal class Mail
{
    public string Text { get; set; }

    public Mail(string text)
    {
        Text = text;
    }
}

答案 4 :(得分:0)

我会使用一个线程来完成整个过程。那就是生成邮件正文并发送。仅仅因为生成邮件正文不会花费时间,而是发送电子邮件。

此外,如果您使用的是Windows附带的SMTP服务器,那么您只需将电子邮件放入队列文件夹中,smtp服务器就会负责发送电子邮件。

所以你可以启动多个线程(保持数字上限)每个线程完成它的工作。如果您正在处理一组作业(数据),那么您可以将数据并行化(即将数据拆分为与系统核心数量相匹配的块,例如拍摄作业(线程)。 / p>

使用任务将使所有这一切变得更加简单,即两个线程发送一个电子邮件或一个线程来完成整个作业,但使用多个线程并行执行多个作业。