简体   繁体   English

在BlockingCollection生产者消费者中完成与IDisposable的比较

[英]Finalize vs. IDisposable in BlockingCollection Producer Consumer

I have a simple logger with producer consumer pattern based on BlockingCollection (code is below). 我有一个带有基于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'
        }
    }
}

The problem is that application is not ending instantly with that. 问题在于应用程序并不会因此而立即结束。 It takes 20-40 seconds to end an application and if I pause it with debugger in a middle, I see that: 结束应用程序需要20-40秒,如果我在中间用调试器暂停它,我会看到:
1. GC.Finalize thread is set on _worker.Join(); 1. GC.Finalize线程设置在_worker.Join();上。
2. _worker thread is on _messages.Take(). 2. _worker线程位于_messages.Take()上。

I would await that _messages.Take() is ended short after _messages.CompleteAdding(); 我等待_messages.Take()在_messages.CompleteAdding()之后不久结束。 But looks like it is not. 但看起来并非如此。

What's wrong with this finalization and how to better finalize worker thread in this situation? 这种完成有什么问题,在这种情况下如何更好地完成工作线程?

PS I could simply drop _worker.Join() but then Work() can write something to closed file. PS我可以简单地删除_worker.Join(),但是Work()可以将某些内容写入关闭的文件。 I mean, this is concurrent non determined situation then. 我的意思是,这是并发的不确定情况。

Update 更新资料
As a proof of concept I've renamed ~Logger() to Close() and call it at some point. 作为概念证明,我已将〜Logger()重命名为Close()并在某个时候调用它。 It closes logger instantly. 它立即关闭记录器。 So _messages.Take() is ending right after _messages.CompleteAdding() as expected in this case. 因此,在这种情况下,_messages.Take()在_messages.CompleteAdding()之后立即结束。

The only explanation of the 20-40 seconds delay in ~Logger I see in high priority of the GC thread. 我在GC线程的高优先级中看到了〜Logger中20-40秒延迟的唯一解释。 Could there be another explanation? 可能还有其他解释吗?

In C#, Finalizers (aka destructors) are non-deterministic, which means you cannot predict when they will be called or in what order. 在C#中, 终结器 (即析构函数)是不确定的,这意味着您无法预测何时调用它们或调用它们的顺序。 For example in your code, it's entirely possible for the finalizer of _worker to be before after the finalizer for Logger. 例如,在您的代码中,_worker的终结器完全有可能 Logger的终结器之后。 For this reason, you should never access managed objects (such as FileStreams etc) inside a finalizer, because the finalizers of other managed resources could have already completed, making their references invalid. 因此,您永远不要访问终结器内部的托管对象(如FileStreams等),因为其他托管资源的终结器可能已经完成,从而使它们的引用无效。 Also the finalizer will not be called until after the GC determines that a collection is necessary (due to the need for additional memory). 另外,在GC确定需要收集数据之前(由于需要额外的内存),将不会调用终结器。 In your case, the GC probably takes 20-40 seconds before it makes the required collection(s). 在您的情况下,GC可能需要20到40秒才能完成所需的收集。

What you want to do is get rid of the finalizer and use the IDisposable interface instead (optionally with a Close() method that might provide better readability). 您想要做的是摆脱终结器,改而使用IDisposable接口(可选地使用Close()方法,它可能会提供更好的可读性)。

Then you would just call logger.Close() when it is no longer required. 然后,当不再需要它时,只需调用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
}

In general, only use a finalizer when you have unmanaged resources to clean up (for example, if you are using P/Invoke WinAPI function calls etc). 通常,仅在有非托管资源需要清理时才使用终结器(例如,如果您正在使用P / Invoke WinAPI函数调用等)。 If you are using only .Net classes, etc. you probably do not have any reason to use one. 如果仅使用.Net类等,则可能没有任何理由使用一个。 IDisposable is almost always the better choice, because it provides deterministic cleanup. IDisposable几乎总是更好的选择,因为它提供了确定性的清除功能。

For more information on finalizers vs destructors, take a look here: What is the difference between using IDisposable vs a destructor in C#? 有关终结器与析构函数的更多信息,请看这里: 在C#中使用IDisposable与析构函数有什么区别?

Another change I would make in your code is using TryTake instead of Take. 我会在您的代码中进行的另一个更改是使用TryTake而不是Take。 This gets rid of the need for the try/catch because it will not throw an exception when the collection is empty and CompleteAdding is called. 这消除了对try / catch的需要,因为当集合为空并且调用CompleteAdding时,它不会引发异常。 It will simply return false. 它只会返回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'       
}

The two exceptions you catch in your code can still occur for other reasons such as accessing it after it is disposed or modifying the BlockingCollection's underlying collection (see MSDN for more info). 你赶上两个例外在你的代码仍然出现因其他原因,如它被设置后访问它或修改BlockingCollection的基础集合(参见MSDN获取更多信息)。 But neither of those should occur in your code, because you don't hold a reference to the underlying collection, and you don't dispose of the BlockingCollection before the Work function is complete. 但是这些都不应该出现在代码中,因为您没有持有对基础集合的引用,并且在Work函数完成之前也没有处置BlockingCollection。 If you still wanted to catch those exceptions, just in case, you can place a try/catch block outside of the while loop (because you would NOT want to continue the loop after either exception occurs). 如果您仍然想捕获这些异常,以防万一,可以将try / catch块放在 while循环之外(因为您不想在任何一个异常发生后继续循环)。

Finally, why do you specify int.MaxValue as the collection's capacity? 最后,为什么要指定int.MaxValue作为集合的容量? You shouldn't do this unless you expect to routinely add close to that many messages to the collection. 除非希望定期向集合中添加那么多的消息,否则不应该这样做。

So altogether, I would re-write your code as follows: 因此,总而言之,我将按照以下方式重新编写您的代码:

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'
    }
}

As you can see, I added Start() and Stop() methods to enable/disable queue processing. 如您所见,我添加了Start()Stop()方法来启用/禁用队列处理。 If you want, you can call Start() from your constructor, but in general, you probably don't want expensive operations (such as thread creation) in a constructor. 如果需要,可以从构造函数中调用Start(),但通常来说,您可能不希望构造函数中执行昂贵的操作(例如创建线程)。 I used Start/Stop instead of Open/Close, because it seemed to make more sense for a logger, but that's just a personal preference, and either pair would work fine. 我使用“开始/停止”而不是“打开/关闭”,因为这对于记录器来说似乎更有意义,但这只是个人喜好,并且两者都可以正常工作。 As I mentioned before, you don't even have to use a Stop or Close method. 如前所述,您甚至不必使用Stop或Close方法。 Simply adding Dispose() is enough, but some classes (like Stream s etc) use Close or Stop as an alias for Dispose just to make the code more readable. 只需添加Dispose()就足够了,但是某些类(如Stream等)使用Close或Stop作为Dispose的别名,只是为了使代码更具可读性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 带有BlockingCollection的消费者/生产者看起来很慢 - Consumer/Producer with BlockingCollection appears slow 生产者/消费者,BlockingCollection和等待更改 - Producer/Consumer, BlockingCollection, and waiting for changes 如何使用BlockingCollection <>解决生产者/消费者竞争条件 - How to solve producer/consumer race condition with BlockingCollection<> 生产者消费者有单独的类,具有共同的BlockingCollection - Producer Consumer Separate Classes with common BlockingCollection 无需BlockingCollection的简单生产者-消费者集合 - Simple producer-consumer collection without BlockingCollection 如何实现BlockingCollection来修复此生产者/消费者问题? - How to implement BlockingCollection to fix this Producer/Consumer issue? BlockingCollection vs Subject用作消费者 - BlockingCollection vs Subject for use as a consumer C#BlockingCollection生成者使用者而不阻塞使用者线程 - C# BlockingCollection producer consumer without blocking consumer thread 使用阻塞收集和任务的经典生产者消费者模式 .net 4 TPL - classic producer consumer pattern using blockingcollection and tasks .net 4 TPL 使用 BlockingCollection 通过生产者-消费者模式保存图像 - Saving images via producer-consumer pattern using BlockingCollection
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM