ファイナライザーを使用しない理由

少し前まで、ファイナライザーのチェックに関連する診断に取り組みました。同僚と私は、ガベージコレクターの詳細とオブジェクトのファイナライズについて議論しました。 そして、私はC#で5年以上開発してきましたが、共通の意見には至らず、この問題をさらに詳しく検討することにしました。











はじめに



通常、.NET開発者は、アンマネージリソースを解放する必要があるときに、最初にファイナライザーを知るようになります。 問題は、何を使用すべきかです:クラスにIDisposableを実装するか、ファイナライザを追加しますか? 次に、たとえばStackOverflowに移動し、 C#のFinalize / Disposeパターンのような質問への回答を読みます。これは、ファイナライザー定義と組み合わせて、古典的なIDisposable実装パターンについて説明します。 同じパターンは、MSDNのIDisposableインターフェイスの説明にあります。 マネージドリソースとアンマネージドリソースのクリーニングを個別のメソッドで実装したり、アンマネージドリソースを解放するためのラッパークラスを作成するなどのオプションを理解して提供するのはかなり難しいと考える人もいます。 StackOverflowの同じページにあります。



これらのメソッドのほとんどには、ファイナライザの実装が含まれます。 これがもたらす利点と潜在的な問題を見てみましょう。



ファイナライザーを使用することの長所と短所



長所



  1. ファイナライザを使用すると、ガベージコレクタによって削除される前にオブジェクトをクリーンアップできます。 開発者がオブジェクトでDispose()メソッドを呼び出すのを忘れた場合、ファイナライザでアンマネージリソースを解放して、リークを回避できます。


おそらくそれだけです。 これが唯一のプラスであり、さらには議論の余地があります。



短所
  1. ファイナライズは非決定的です。 ファイナライザがいつ呼び出されるかはわかりません。 CLRがオブジェクトのファイナライズを開始する前に、ガベージコレクターは次のガベージコレクションの開始時にファイナライズの準備ができているオブジェクトのキューにオブジェクトを配置する必要があります。 そして、この瞬間は定義されていません。



  2. ファイナライザを備えたオブジェクトはガベージコレクタによってすぐに削除されないため、オブジェクトとそれに関連付けられたオブジェクトのグラフ全体がガベージコレクションを生き延び、次世代に落ちます。 ガベージコレクターがこの世代のオブジェクトを収集することを決定すると、これらはすぐに削除されます。



  3. ファイナライザは他のアプリケーションスレッドの作業と並行して別のスレッドで実行されるため、ファイナライズを必要とする新しいオブジェクトを古いオブジェクトのファイナライザが機能するよりも速く作成できる場合があります。 これにより、メモリ消費量が増加し、パフォーマンスが低下し、場合によっては、アプリケーションがOutOfMemoryExceptionでクラッシュします。 さらに、開発者のマシンでは、たとえばプロセッサの数が少なく、オブジェクトの作成が遅くなるか、戦闘状態である限りアプリケーションが動作せず、メモリが不足するため、この状況に遭遇することはありません。 多くの時間を費やして、理由がファイナライザーにあったことを理解できます。 このマイナスは、おそらく、単一のプラスの利点と重なります。



  4. ファイナライザの実行中に例外が発生した場合、アプリケーションは緊急に終了します。 したがって、ファイナライザを実装するときは、特に注意する必要があります。ファイナライザを既に呼び出すことができる他のオブジェクトのメソッドにアクセスしないでください。 ファイナライザが別のスレッドで呼び出されることを考慮してください。 nullになる可能性のある他のすべてのオブジェクトをnullでチェック 最後の規則は、完全に初期化されていなくても、その状態のいずれかのオブジェクトに対してファイナライザを呼び出すことができるという事実に関連しています。 たとえば、クラスフィールドのコンストラクターで常に新しいオブジェクトを割り当て、ファイナライザーで常にnullでなくアクセスすることを期待している場合、実行前に基本クラスコンストラクターでオブジェクトを作成中に例外が発生した場合、 NullReferenceExceptionを取得できますあなたのデザイナーはそれを得ませんでした。



  5. ファイナライザはまったく実行されない場合があります。 たとえば、前の段落で説明した理由で外国のファイナライザーで例外が発生した場合、アプリケーションが緊急に完了した場合、他のすべてのファイナライザーは実行されません。 ファイナライザでオペレーティングシステムのアンマネージオブジェクトを解放すると、アプリケーションが完了したときにシステム自体がリソースを返すという意味で、何も悪いことは起こりません。 ただし、ファイル内の未書き込みバイトを破棄すると、データが失われます。 したがって、おそらくファイナライザを実装せず、 Dispose()の呼び出しを忘れた場合は常にデータの損失を許可する方が適切です。この場合、問題の検出が容易になるためです。



  6. ファイナライザーは1回だけ呼び出されることを覚えておく必要があります。別の生きているオブジェクトに参照を割り当ててファイナライザーのオブジェクトを復活させる場合、 GCメソッドを使用してファイナライズのために再度登録する必要があります。 ReRegisterForFinalize()



  7. アプリケーションがシングルスレッドであっても、競合状態など、マルチスレッドアプリケーションの問題に遭遇する可能性があります。 このケースは絶対にエキゾチックですが、理論的には可能です。 オブジェクトにファイナライザがあり、別のオブジェクトがそのオブジェクトへの参照を保持しており、ファイナライザがあるとします。 両方のオブジェクトがガベージコレクターで使用可能になり、それらのファイナライザーが実行を開始し、他のオブジェクトが復活すると、オブジェクトとオブジェクトが再びアクティブになります。 オブジェクトのメソッドがメインスレッドから呼び出され、同時にファイナライザから呼び出されると、ファイナライズの準備ができたオブジェクトのキューに残ったままになる可能性があります。 この例を再現するコードを以下に示します。 Rootオブジェクトのファイナライザーが最初に実行され、次にNestedオブジェクトのファイナライザーが実行され、その後DoSomeWork()メソッドが2つのスレッドから一度に呼び出される方法を確認できます。


サンプルコード
class Root { public volatile static Root StaticRoot = null; public Nested Nested = null; ~Root() { Console.WriteLine("Finalization of Root"); StaticRoot = this; } } class Nested { public void DoSomeWork() { Console.WriteLine(String.Format( "Thread {0} enters DoSomeWork", Thread.CurrentThread.ManagedThreadId)); Thread.Sleep(2000); Console.WriteLine(String.Format( "Thread {0} leaves DoSomeWork", Thread.CurrentThread.ManagedThreadId)); } ~Nested() { Console.WriteLine("Finalization of Nested"); DoSomeWork(); } } class Program { static void CreateObjects() { Nested nested = new Nested(); Root root = new Root(); root.Nested = nested; } static void Main(string[] args) { CreateObjects(); GC.Collect(); while (Root.StaticRoot == null) { } Root.StaticRoot.Nested.DoSomeWork(); Console.ReadLine(); } }
      
      





これが私のマシンに表示されるものです:



 Finalization of Root Finalization of Nested Thread 10 enters DoSomeWork Thread 2 enters DoSomeWork Thread 10 leaves DoSomeWork Thread 2 leaves DoSomeWork
      
      





ファイナライザーが異なる順序で呼び出される場合、 ネストされた ルートルートの作成を逆にしてみてください。



結論



.NETのファイナライザは、自分の足で撃つのが最も簡単な場所です。 急いでIDisposableを実装するすべてのクラスにファイナライザーを追加する前に、それらが本当に必要かどうかを検討する必要があります。 CLR開発者自身が、[ パターン破棄]ページでの使用に注意していることに注意する必要があります。 ファイナライザが必要と思われる場合は慎重に検討してください。 パフォーマンスとコードの複雑さの両方の観点から、ファイナライザーを使用したインスタンスには実際のコストがかかります。



ただし、ファイナライザを使用することに決めた場合は、PVS-Studioで潜在的なエラーを見つけることができます。 NullReferenceExceptionが発生する可能性のあるファイナライザのすべての場所を表示するV3100診断プログラムがあります。



All Articles