簡體   English   中英

如何使用BlockingCollection <>解決生產者/消費者競爭條件

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

我正在實現一個記錄器,它將記錄寫入數據庫。 為了防止數據庫寫入阻塞調用記錄器的代碼,我已經將數據庫訪問移動到一個單獨的線程,使用基於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() ,因此我們現在處於ProcessBufferforeach循環體中。
  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實現以避免這種競爭條件?

注意:由於各種原因,我必須從頭開始編寫記錄器,所以請不要回答我使用現有記錄框架的建議。

首先永遠不要鎖定公共對象 ,尤其是this

此外, 永遠不要使用裸布線進行同步 :如果您想了解可能出現的問題請查看我的博客: 同步,內存可見性和漏洞抽象 :)

關於這個問題本身我一定會遺漏一些東西,但為什么你需要這樣的Flush方法呢?

實際上,當您完成日志記錄后,您將通過從主線程調用其Dispose方法來處置記錄器。

並且您已經以這樣的方式實現它,它將等待“寫入DB”任務。

如果我錯了,你真的需要與另一個原語同步,那么你應該使用一個事件:

DbLogger

public ManualResetEvent finalizing { get; set; }

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

在某處,例如在ProcessBuffer ,當您完成對DB的寫入時,您會通知:

finalizing.Set();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM