C# 如何利用BlockingCollection解决生产者/消费者竞争状况<&燃气轮机;

C# 如何利用BlockingCollection解决生产者/消费者竞争状况<&燃气轮机;,c#,.net,multithreading,thread-safety,C#,.net,Multithreading,Thread Safety,我正在实现一个将记录写入数据库的记录器。为了防止数据库写入阻塞调用记录器的代码,我将DB访问移到了一个单独的线程,使用基于BlockingCollection的生产者/消费者模型实现 以下是简化的实现: abstract class DbLogger : TraceListener { private readonly BlockingCollection<string> _buffer; private readonly Task _writerTask;

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

以下是简化的实现:

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); 
    }
}
public void Flush()
{
    // Sleep until the buffer is empty.
    while(this._buffer.Count > 0)
    {
        Thread.Sleep(50);
    }
}
此实现的问题在于以下事件顺序:

  • 缓冲区中有一个条目
  • 在日志线程中,枚举器上调用了
    MoveNext()
    ,因此我们现在处于
    ProcessBuffer
    foreach
    循环体中
  • Flush()
    由主线程调用。它看到集合为空,因此立即返回
  • 主线程关闭数据库连接
  • 回到日志线程中,
    foreach
    循环的主体开始执行<调用code>WriteToDb,但由于数据库连接已关闭而失败
  • 因此,我的下一次尝试是添加一些标志,如:

    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()
    方法可以在集合为空之后但
    \u isWritingBuffer
    设置为
    true
    之前执行

    如何修复我的
    Flush
    实现以避免这种争用情况


    注意:由于各种原因,我必须从头开始编写记录器,因此请不要建议我使用一些现有的日志框架。

    首先永远不要锁定公共对象,尤其是
    这个

    此外,永远不要使用纯布尔值进行同步:如果您想了解可能出现的问题,请查看我的博客::)

    关于问题本身,我肯定遗漏了一些东西,但为什么您需要这样一种
    Flush
    方法

    实际上,当您完成日志记录时,您将通过从主线程调用其
    dispose
    方法来处理记录器

    您已经以这样的方式实现了它,它将等待“write to DB”任务

    如果我错了,并且您确实需要与另一个原语同步,那么您应该使用一个事件:

    DbLogger
    中:

    public ManualResetEvent finalizing { get; set; }
    
    public void Flush()
    {
        finalizing.WaitOne();
    }
    
    在某个地方,例如在
    ProcessBuffer
    中,当您完成对数据库的写入时,您会通知:

    finalizing.Set();
    

    为什么不锁定(这个)这个呢;并锁定(这个)这个。_isWritingBuffer=false;在foreach循环之外?因此,您假设在知道集合为空之前一直在写?@tolanj:这不起作用,因为枚举器正在阻塞。因此,如果集合中没有任何内容,那么它就放在那里(直到
    CompleteAdding()为止)调用了
    。如果我将锁移到foreach循环之外,
    Flush
    将永远不会返回,直到记录器被释放。我不了解您的所有代码,但为什么您要这么难?文档中有一些简单的示例。我想您是对的-我可能根本不需要Flush方法。我只需要确保记录器在DB连接消失之前处理(目前,这两个操作的顺序是任意的)。至于您的前两个注释,我已经更正了它们(它们仅在我发布的原始示例中不正确)