我有一个基于BlockingCollection的生产者消费者模式的简单记录器(代码如下)。
public class Logger
{
public Logger()
{
_messages = new BlockingCollection<LogMessage>(int.MaxValue);
_worker = new Thread(Work) {IsBackground = true};
_worker.Start();
}
~Logger()
{
_messages.CompleteAdding();
_worker.Join(); // Wait for the consumer's thread to finish.
//Some logic on closing log file
}
/// <summary>
/// This is message consumer thread
/// </summary>
private void Work()
{
while (!_messages.IsCompleted)
{
//Try to get data from queue
LogMessage message;
try
{
message = _messages.Take();
}
catch (ObjectDisposedException) { break; } //The BlockingCollection(Of T) has been disposed.
catch(InvalidOperationException){ continue; } //the BlockingCollection(Of T) is empty and the collection has been marked as complete for adding.
//... some simple logic to write 'message'
}
}
}
问题是应用程序没有立即结束。结束一个应用程序需要20-40秒,如果我在调试器中间暂停它,我会看到:
1. GC.Finalize线程在_worker.Join()上设置;
2. _worker线程在_messages.Take()上。
我会等待_messages.Take()在_messages.CompleteAdding()之后结束。但看起来并非如此。
最终确定有什么问题以及如何在这种情况下更好地完成工作线程?
P.S。我可以简单地删除_worker.Join()但是然后Work()可以写一些东西到封闭文件。我的意思是,这是同时未确定的情况。
更新
作为概念证明,我将~Logger()重命名为Close()并在某个时刻调用它。它会立即关闭记录器。所以_messages.Take()在_messages.CompleteAdding()之后正好结束,在这种情况下是预期的。
我在GC线程的高优先级中看到的~Logger 20-40秒延迟的唯一解释。可以有另一种解释吗?
答案 0 :(得分:3)
在C#中,终结者(又称析构函数)是非确定性的,这意味着您无法预测它们何时被调用或以何种顺序被调用。例如,在您的代码中,_worker的终结器完全有可能在Logger的终结器之后之前。因此,您永远不应该在终结器中访问托管对象(例如FileStreams等),因为其他托管资源的终结器可能已经完成,使其引用无效。 在GC确定需要集合之后(由于需要额外的内存),才会调用终结器。在您的情况下,GC可能需要20-40秒才能进行所需的收集。
你想要做的就是摆脱终结器并使用IDisposable接口(可选择使用可提供更好可读性的Close()方法)。
然后,当不再需要时,您只需致电logger.Close()
。
void IDisposable.Dispose()
{
Close();
}
void Close()
{
_messages.CompleteAdding();
_worker.Join(); // Wait for the consumer's thread to finish.
//Some logic on closing log file
}
通常,只有在使用非托管资源进行清理时才使用终结器(例如,如果您正在使用P / Invoke WinAPI函数调用等)。如果您只使用.Net类等,则可能没有任何理由使用它。 IDisposable几乎总是更好的选择,因为它提供了确定性清理。
有关终结器与析构函数的更多信息,请查看此处: What is the difference between using IDisposable vs a destructor in C#?
我将在您的代码中进行的另一项更改是使用TryTake而不是Take。这消除了对try / catch的需要,因为当集合为空并且调用CompleteAdding时它不会抛出异常。它只会返回false。
private void Work()
{
//Try to get data from queue
LogMessage message;
while (_messages.TryTake(out message, Timeout.Infinite))
//... some simple logic to write 'message'
}
您在代码中捕获的两个例外仍然会出于其他原因,例如在处理它之后访问它或修改BlockingCollection的基础集合(有关详细信息,请参阅MSDN)。但是这些都不会出现在您的代码中,因为您没有对底层集合的引用,并且在Work函数完成之前您不会丢弃BlockingCollection。
如果您仍想捕获这些异常,以防万一,您可以在while循环的之外放置一个try / catch块(因为您不希望在发生任何异常后继续循环)。 / p>
最后,为什么要指定int.MaxValue作为集合的容量?您不应该这样做,除非您希望定期向该集合添加接近那么多消息。 总而言之,我会按如下方式重新编写代码: 如您所见,我添加了public class Logger : IDisposable
{
private BlockingCollection<LogMessage> _messages = null;
private Thread _worker = null;
private bool _started = false;
public void Start()
{
if (_started) return;
//Some logic to open log file
OpenLogFile();
_messages = new BlockingCollection<LogMessage>(); //int.MaxValue is the default upper-bound
_worker = new Thread(Work) { IsBackground = true };
_worker.Start();
_started = true;
}
public void Stop()
{
if (!_started) return;
// prohibit adding new messages to the queue,
// and cause TryTake to return false when the queue becomes empty.
_messages.CompleteAdding();
// Wait for the consumer's thread to finish.
_worker.Join();
//Dispose managed resources
_worker.Dispose();
_messages.Dispose();
//Some logic to close log file
CloseLogFile();
_started = false;
}
/// <summary>
/// Implements IDiposable
/// In this case, it is simply an alias for Stop()
/// </summary>
void IDisposable.Dispose()
{
Stop();
}
/// <summary>
/// This is message consumer thread
/// </summary>
private void Work()
{
LogMessage message;
//Try to get data from queue
while(_messages.TryTake(out message, Timeout.Infinite))
WriteLogMessage(message); //... some simple logic to write 'message'
}
}
Start()
和Stop()
方法来启用/禁用队列处理。如果需要,可以从构造函数中调用Start(),但一般情况下,您可能不希望在构造函数中执行昂贵的操作(如创建线程)。我使用Start / Stop而不是Open / Close,因为它似乎对记录器更有意义,但这只是个人偏好,并且任何一对都可以正常工作。正如我之前提到的,您甚至不必使用Stop或Close方法。简单地添加Dispose()就足够了,但有些类(如Stream
等)使用Close或Stop作为Dispose的别名,只是为了使代码更具可读性。