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

时间:2014-08-06 18:59:04

标签: c# .net multithreading thread-safety

我正在实现一个将记录写入数据库的记录器。为了防止数据库写入阻塞调用记录器的代码,我已将数据库访问权移动到一个单独的线程,使用基于BlockingCollection<string>的生产者/消费者模型实现。

这是简化的实施:

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); 
    }
}

现在,当我的应用程序关闭时,我需要确保在数据库连接断开之前刷新缓冲区。否则,WriteToDb将抛出异常。

所以,这是我天真的Flush实施:

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

此实现的问题是以下事件序列:

  1. 缓冲区中有一个条目。
  2. 在记录线程中,在枚举器上调用MoveNext(),因此我们现在位于ProcessBuffer foreach循环的正文中。
  3. Flush()由主线程调用。它看到该集合是空的,所以立即返回。
  4. 主线程关闭数据库连接。
  5. 回到日志记录线程中,foreach循环的主体开始执行。调用WriteToDb,因为数据库连接已关闭而失败。
  6. 所以,我的下一次尝试是添加一些标志,如下所示:

    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);
        }
    }
    

    但是,仍然存在竞争条件,因为整个Flush()方法可以在集合为空之后但_isWritingBuffer设置为true之前执行。

    如何修复我的Flush实施以避免这种竞争条件?

    注意:由于各种原因,我必须从头开始编写记录器,所以请不要回答我使用现有记录框架的建议。

1 个答案:

答案 0 :(得分:5)

首先永远不会锁定公共对象,尤其是this

此外从未使用裸布线进行同步:如果您希望了解可能出现的问题,请参阅我的博客: Synchronization, memory visibility and leaky abstractions :)< / p>

关于这个问题本身我一定会遗漏一些东西,但为什么你需要这样的Flush方法?

确实,当您完成日志记录后,您将通过从主线程调用其Dispose方法来处置记录器。

你已经以这样的方式实现它,它将等待&#34;写入DB&#34;任务。

如果我错了,你真的需要与另一个原语同步,那么你应该使用一个事件:

DbLogger

public ManualResetEvent finalizing { get; set; }

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

在某处,例如在ProcessBuffer中,当您完成写入数据库时​​,您会通知

finalizing.Set();