0

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 秒かかります。途中でデバッガーを一時停止すると、次のことが
わかります。
2. _worker スレッドは _messages.Take() にあります。

_messages.Take() が _messages.CompleteAdding(); の直後に終了するのを待ちます。しかし、そうではないようです。

このファイナライズの何が問題で、この状況でワーカー スレッドをより適切にファイナライズする方法を教えてください。

PS私は単純に _worker.Join() を削除できますが、 Work() は閉じたファイルに何かを書き込むことができます。つまり、これは同時非決定状況です。

更新
概念の証明として、名前を ~Logger() から Close() に変更し、ある時点で呼び出します。ロガーを即座に閉じます。したがって、この場合、予想どおり _messages.Take() は _messages.CompleteAdding() の直後に終了します。

~Logger での 20 ~ 40 秒の遅延の唯一の説明は、GC スレッドの優先度が高いことです。別の説明がありますか?

4

1 に答える 1

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#?

あなたのコードに加えるもう 1 つの変更は、Take の代わりに TryTake を使用することです。これにより、コレクションが空で CompleteAdding が呼び出されたときに例外がスローされないため、try/catch の必要がなくなります。単純に 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'       
}

コードでキャッチした 2 つの例外は、破棄された後にアクセスしたり、BlockingCollection の基になるコレクションを変更したりするなど、他の理由で引き続き発生する可能性があります (詳細については、 MSDNを参照してください)。ただし、基になるコレクションへの参照を保持せず、Work 関数が完了する前に BlockingCollection を破棄しないため、これらのいずれもコード内で発生しないはずです。それでもこれらの例外をキャッチしたい場合は、while ループの外側に try/catch ブロックを配置できます (いずれかの例外が発生した後にループを続行したくないため)。

最後に、コレクションの容量として 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() を呼び出すことができますが、一般的に、コンストラクターでの高価な操作 (スレッドの作成など) は望ましくありません。Open/Close の代わりに Start/Stop を使用しました。ロガーにとってはより理にかなっているように思えたからですが、これは単なる個人的な好みであり、どちらのペアでも問題なく機能します。前に述べたように、Stop または Close メソッドを使用する必要さえありません。Dispose() を追加するだけで十分ですが、一部のクラス ( Streams など) では、コードを読みやすくするために、Dispose のエイリアスとして Close または Stop を使用します。

于 2012-12-27T05:51:41.973 に答える