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

标签 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被调用,但由于数据库连接已关闭而失败。

所以,我的下一个尝试是添加一些标志,如下所示:

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实现以避免这种竞争条件?

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

最佳答案

首先永远不要锁定公共(public)对象,尤其是this

此外,永远不要使用纯 bool 值进行同步:如果您想了解可能出错的地方,请参阅我的博客: Synchronization, memory visibility and leaky abstractions :)

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

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

并且您已经以等待“写入数据库”任务的方式实现它。

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

DbLogger 中:

public ManualResetEvent finalizing { get; set; }

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

在某个地方,例如在 ProcessBuffer 中,您会在完成写入 DB 时收到通知:

finalizing.Set();

关于c# - 如何使用 BlockingCollection<> 解决生产者/消费者竞争条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25168113/

相关文章:

c# - 具有多个模型的 Entity Framework 抛出错误,说明 "The model backing the <modelDbContext> context has changed..."

c# - 在 mathdotnet 中解析具有双系数值的表达式

c# - C#中_bstr_t的等价物是什么

c# - 多线程无法正常执行

c# - 将 UWP 应用程序最小化到系统托盘

c# - 使用多个存储过程将大量数据添加到数据库中

c# - 析构函数 - 如果应用程序崩溃,它会被调用吗

c# - FtpWebRequest 移动文件

java - 处理序列化类中包含 transient 成员的线程的正确做法

multithreading - Elasticsearch 中的锁