多生产者和一个消费者的相互排斥

时间:2015-07-07 20:27:25

标签: c# multithreading web-services mutual-exclusion

我有一个有趣的问题,我需要解决一些生产代码。我们目前正在开发一种Web服务,该服务将从许多不同的应用程序中调用,主要用于发送电子邮件。每当发送新电子邮件时,我们最终都需要将该电子邮件的收据添加到数据库中,但理想情况下我们不想立即执行此操作,因此我们将随着时间的推移建立缓冲区。一旦缓冲区达到一定长度,或经过足够的时间后,缓冲区的内容将被刷新到数据库中。

可以这样想,当一个线程发送一封电子邮件时,它会锁定缓冲区,以便在不受干扰的情况下添加它的日志并保持线程安全。如果它看到缓冲区具有一定的大小(在本例中我们将说1000),那么线程将其全部写入数据库的责任(我认为这是低效的,但我是使用Service Stack作为我们的Web框架,所以如果有一种委托这个任务的方法,我宁愿采用这种方法。)

现在,由于写入数据库可能非常耗时,我们希望添加要使用的辅助缓冲区。因此,一旦一个缓冲区已满,所有新请求都会将其工作记录到第二个缓冲区中,同时刷新第一个缓冲区。类似地,一旦第二个缓冲区已满,所有线程将移回第一个缓冲区,第二个缓冲区将被刷新。

我们需要解决的主要问题:

  • 当一个线程决定需要刷新其中一个缓冲区时,它需要指示所有新线程开始记录到第二个缓冲区(这应该像改变一些变量或指针指向空缓冲区一样简单)< / LI>
  • 如果当前关键部分的当前用户决定刷新日志时当前线程被阻止,则需要重新激活所有被阻止的线程并将其指向第二个缓冲区

我更关心第二个要点。重新唤醒所有被阻塞的线程的最佳方法是什么,但是不是让它们进入第一个缓冲区的临界区,而是让它们试图获得空的锁定?

修改

根据以下评论,我想出了一些我认为可行的方法。我不知道存在线程安全的数据结构。

    private readonly ConcurrentQueue<EmailResponse> _logBuffer = new ConcurrentQueue<EmailResponse>();
    private readonly object _lockobject = new object();
    private const int BufferThreshold = 1000;

    public void AddToBuffer(EmailResponse email)
    {
        _logBuffer.Enqueue(email);

        Monitor.Enter(_lockobject);
        if (_logBuffer.Count >= BufferThreshold)
            Task.Run(async () =>
            {
                EmailResponse response;
                for (var i = 0; i < BufferThreshold; i++)
                    if (_logBuffer.TryDequeue(out response))
                        await AddMail(response);
                Monitor.Exit(_lockobject);
            });
        else Monitor.Exit(_lockobject);
    }

2 个答案:

答案 0 :(得分:2)

我不确定你是否需要第二个缓冲区; draw(false)让我觉得这是解决问题的好方法。每个线程都可以排队而不会发生冲突,并且如果任何线程注意到队列的Count高于魔术阈值,那么即使其他线程排队了更多,您也可以安全地出列到那么多对象。

我掀起的一个(非常快速和肮脏)的工作样品:

static class Buffer
{
    private const int c_MagicThreshold = 10;
    private static ConcurrentQueue<string> s_Messages = new ConcurrentQueue<string>();
    private static object s_LockObj = new object();

    public static void Enqueue(string message)
    {
        s_Messages.Enqueue(message);
        // try to flush every time; spawn on a non-blocking thread and immediately return
        new Task(Flush).Start();
    }

    public static void Flush()
    {
        // do we flush at all?
        if (s_Messages.Count >= c_MagicThreshold)
        {
            lock (s_LockObj)
            {
                // make sure another thread didn't flush while we were waiting
                if (s_Messages.Count >= c_MagicThreshold)
                {
                    List<string> messages = new List<string>();
                    Console.WriteLine("Flushing " + c_MagicThreshold + " messages...");
                    for (int i = 0; i < c_MagicThreshold; i++)
                    {
                        string message;
                        if (!s_Messages.TryDequeue(out message))
                        {
                            throw new InvalidOperationException("How the hell did you manage that?");
                            // or just break from the loop if you don't care much, you spaz
                        }
                        messages.Add(message);
                    }
                    Console.WriteLine("[ " + String.Join(", ", messages) + " ]");

                    // number of new messages enqueued between threshold pass and now
                    Console.WriteLine(s_Messages.Count + " messages remaining in queue");
                }
            }
        }
    }
}

测试电话:

Parallel.For(0, 30, (i) =>
{
    Thread.Sleep(100);  // do other things
    Buffer.Enqueue(i.ToString());
});

测试运行的控制台输出:

  

刷新10条消息......

     

[28,21,14,0,7,29,8,15,1,22]

     

队列中剩余5条消息

     

刷新10条消息......

     

[16,3,9,2,23,17,10,4,24,5]

     

队列中剩余1条消息

     

刷新10条消息......

     

[11,18,25,19,26,12,6,20,13,27]

     

队列中剩余0条消息

答案 1 :(得分:0)

你能给每个线程一个持有两个缓冲区并让线程记录到这个对象的对象吗?然后,当每个线程要求它记录某些内容时,该对象将决定写入哪个缓冲区。此对象也可能负责将完整缓冲区清空到数据库,而不是阻止线程写入。