简体   繁体   English

如何使用BlockingCollection <>解决生产者/消费者竞争条件

[英]How to solve producer/consumer race condition with BlockingCollection<>

I am implementing a logger which writes records to the database. 我正在实现一个记录器,它将记录写入数据库。 In order to prevent the database writes from blocking the code which is calling the logger, I've moved the DB access to a separate thread, implemented using a producer/consumer model based on BlockingCollection<string> . 为了防止数据库写入阻塞调用记录器的代码,我已经将数据库访问移动到一个单独的线程,使用基于BlockingCollection<string>的生产者/消费者模型实现。

Here's the simplified implementation: 这是简化的实现:

abstract class DbLogger : TraceListener
{
    private readonly BlockingCollection<string> _buffer;
    private readonly Task _writerTask;

    DbLogger() 
    {
        this._buffer = new BlockingCollection<string>(new ConcurrentQueue<string>(), 1000);
        this._writerTask = Task.Factory.StartNew(this.ProcessBuffer, TaskCreationOptions.LongRunning);
    }

    // Enqueue the msg.
    public void LogMessage(string msg) { this._buffer.Add(msg); }

    private void ProcessBuffer()
    {
        foreach (string msg in this._buffer.GetConsumingEnumerable())
        {
            this.WriteToDb(msg);
        }
    }

    protected abstract void WriteToDb(string msg);

    protected override void Dispose(bool disposing) 
    { 
        if (disposing) 
        {
            // Signal to the blocking collection that the enumerator is done.
            this._buffer.CompleteAdding();

            // Wait for any in-progress writes to finish.
            this._writerTask.Wait(timeout);

            this._buffer.Dispose(); 
        }
        base.Dispose(disposing); 
    }
}

Now, when my application shuts down, I need to make sure that the buffer is flushed before the database connection goes down. 现在,当我的应用程序关闭时,我需要确保在数据库连接断开之前刷新缓冲区。 Otherwise, WriteToDb will throw an exception. 否则, WriteToDb将抛出异常。

So, here's my naive Flush implementation: 所以,这是我天真的Flush实现:

public void Flush()
{
    // Sleep until the buffer is empty.
    while(this._buffer.Count > 0)
    {
        Thread.Sleep(50);
    }
}

The problem with this implementation is the following sequence of events: 此实现的问题是以下事件序列:

  1. There is one entry in the buffer. 缓冲区中有一个条目。
  2. In the logging thread, MoveNext() is called on the enumerator, so we're now in the body of ProcessBuffer 's foreach loop. 在日志记录线程中,在枚举器上MoveNext() ,因此我们现在处于ProcessBufferforeach循环体中。
  3. Flush() is called by the main thread. Flush()由主线程调用。 It sees that the collection is empty, so returns immediately. 它看到该集合为空,因此立即返回。
  4. The main thread closes the DB connection. 主线程关闭数据库连接。
  5. Back in the logging thread, the body of the foreach loop starts executing. 回到日志记录线程中, foreach循环的主体开始执行。 WriteToDb is called, and fails because the DB connection has been closed. WriteToDb ,因为数据库连接已关闭而失败。

So, my next try was adding some flags, like so: 所以,我的下一次尝试是添加一些标志,如下所示:

private volatile bool _isWritingBuffer = false;
private void ProcessBuffer()
{
    foreach (string msg in this._buffer.GetConsumingEnumerable())
    {
        lock (something) this._isWritingBuffer = true;
        this.WriteToDb(msg);
        lock (something) this._isWritingBuffer = false;
    }
}

public void Flush()
{
    // Sleep until the buffer is empty.
    bool isWritingBuffer;
    lock(something) isWritingBuffer = this._isWritingBuffer;
    while(this._buffer.Count > 0 || isWritingBuffer)
    {
        Thread.Sleep(50);
    }
}

However, there's still a race condition, since the entire Flush() method could execute after the collection is empty but before _isWritingBuffer gets set to true . 但是,仍然存在竞争条件,因为整个Flush()方法可以在集合为空之后但在_isWritingBuffer设置为true之前执行。

How can I fix my Flush implementation to avoid this race condition? 如何修复我的Flush实现以避免这种竞争条件?

Note: For various reasons, I must write the logger from scratch, so please don't answer with a suggestion that I use some existing logging framework. 注意:由于各种原因,我必须从头开始编写记录器,所以请不要回答我使用现有记录框架的建议。

First never ever lock on a public object , especially this . 首先永远不要锁定公共对象 ,尤其是this

Moreover never ever use bare booleans for synchronization : see my blog if you want to have a glimpse in what can go wrong: Synchronization, memory visibility and leaky abstractions :) 此外, 永远不要使用裸布线进行同步 :如果您想了解可能出现的问题请查看我的博客: 同步,内存可见性和漏洞抽象 :)

Concerning the issue itself I must be missing something but why do you need such a Flush method? 关于这个问题本身我一定会遗漏一些东西,但为什么你需要这样的Flush方法呢?

Indeed when you're done with your logging you will dispose of the logger by calling its Dispose method from the main thread. 实际上,当您完成日志记录后,您将通过从主线程调用其Dispose方法来处置记录器。

And you have implemented it in such a way that it will wait for the "write to DB" task. 并且您已经以这样的方式实现它,它将等待“写入DB”任务。

If I'm wrong and you really need to synchronize with another primitive then you should use an event: 如果我错了,你真的需要与另一个原语同步,那么你应该使用一个事件:

In the DbLogger : DbLogger

public ManualResetEvent finalizing { get; set; }

public void Flush()
{
    finalizing.WaitOne();
}

And somewhere, eg in ProcessBuffer you notify when you're done with writing to DB: 在某处,例如在ProcessBuffer ,当您完成对DB的写入时,您会通知:

finalizing.Set();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM