【发布时间】:2014-08-06 18:59:04
【问题描述】:
我正在实现一个将记录写入数据库的记录器。为了防止数据库写入阻塞调用记录器的代码,我已将数据库访问移至单独的线程,使用基于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 实现以避免这种竞争条件?
注意:由于各种原因,我必须从头开始编写记录器,所以请不要回答建议我使用一些现有的记录框架。
【问题讨论】:
-
为什么不直接锁定(this) this._isWritingBuffer = true;并锁定 (this) this._isWritingBuffer = false;在 foreach 循环之外?因此你假设你正在写作,直到你知道集合是空的?
-
@tolanj:那行不通,因为枚举器被阻塞了。因此,如果集合中没有任何内容,它就坐在那里(直到调用
CompleteAdding()。如果我将锁移到 foreach 循环之外,Flush将永远不会返回,直到记录器被处置。 -
我没有遵循你所有的代码,但你为什么要这么难?该文档有一些简单的示例。 msdn.microsoft.com/en-us/library/dd267312(v=vs.110).aspx
标签: c# .net multithreading thread-safety