在BlockingCollection Producer Consumer中完成与IDisposable的结束

时间:2012-12-27 05:21:27

标签: c# multithreading producer-consumer

我有一个基于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秒延迟的唯一解释。可以有另一种解释吗?

1 个答案:

答案 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的别名,只是为了使代码更具可读性。