我正在实现一个将记录写入数据库的记录器。为了防止数据库写入阻塞调用记录器的代码,我已将数据库访问权移动到一个单独的线程,使用基于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);
}
}
此实现的问题是以下事件序列:
MoveNext()
,因此我们现在位于ProcessBuffer
foreach
循环的正文中。Flush()
由主线程调用。它看到该集合是空的,所以立即返回。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
实施以避免这种竞争条件?
注意:由于各种原因,我必须从头开始编写记录器,所以请不要回答我使用现有记录框架的建议。
答案 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();