5

私が作成したライブラリには、.NET SerialPort クラスと同様の機能を実装するクラス DataPort があります。一部のハードウェアと通信し、データがそのハードウェアを介して着信するたびにイベントを発生させます。この動作を実装するために、DataPort は、DataPort オブジェクトと同じ有効期間を持つと予想されるスレッドをスピンアップします。 問題は、DataPort が範囲外になると、ガベージ コレクションが行われないことです。

現在、DataPort は (pInvoke を使用して) ハードウェアと通信し、管理されていないリソースを所有しているため、IDisposable を実装しています。オブジェクトで Dispose を呼び出すと、すべてが正しく行われます。DataPort は、管理されていないリソースをすべて取り除き、ワーカー スレッドを強制終了して消えます。ただし、DataPort をスコープ外に出すだけでは、ガベージ コレクターがファイナライザーを呼び出すことはなく、DataPort はメモリ内で永久に存続します。私はこれが2つの理由で起こっていることを知っています:

  1. ファイナライザーのブレークポイントはヒットしません
  2. SOS.dllは、DataPort がまだ生きていることを教えてくれます

サイドバー:先に進む前に、答えは「Call Dispose() Dummy!」であることはわかっています。しかし、すべての参照をスコープ外に出しても、最終的には正しいことが起こり、ガベージ コレクターは DataPort を取り除く必要があると思います

問題に戻る: SOS.dll を使用すると、DataPort がガベージ コレクションされない理由は、それがスピンアップしたスレッドがまだ DataPort オブジェクトへの参照を持っているためであることがわかります。スレッドが実行しているインスタンス メソッド。実行中のワーカー スレッドガベージ コレクションの対象にならないため、実行中のワーカー スレッドのスコープ内にある参照もガベージ コレクションの対象になりません。

スレッド自体は、基本的に次のコードを実行します。

public void WorkerThreadMethod(object unused)
{
  ManualResetEvent dataReady = pInvoke_SubcribeToEvent(this.nativeHardwareHandle);
  for(;;)
  {
    //Wait here until we have data, or we got a signal to terminate the thread because we're being disposed
    int signalIndex = WaitHandle.WaitAny(new WaitHandle[] {this.dataReady, this.closeSignal});
    if(signalIndex == 1) //closeSignal is at index 1
    {
      //We got the close signal.  We're being disposed!
      return; //This will stop the thread
    }
    else
    {
      //Must've been the dataReady signal from the hardware and not the close signal.
      this.ProcessDataFromHardware();
      dataReady.Reset()
    }
  }
}

Dispose メソッドには、次の (関連する) コードが含まれています。

public void Dispose()
{
  closeSignal.Set();
  workerThread.Join();
}

スレッドは gc ルートであり、DataPort への参照を保持しているため、DataPort はガベージ コレクションの対象にはなりません。ファイナライザーが呼び出されることはないため、クローズ シグナルをワーカー スレッドに送信することはありません。ワーカー スレッドはクローズ シグナルを受信しないため、その参照を永久に保持し続けます。アック!

この問題に対して私が考えることができる唯一の答えは、WorkerThread メソッドの「this」パラメーターを取り除くことです (以下の回答で詳しく説明します)。他の誰かが別のオプションを考えることができますか? オブジェクトの寿命が同じスレッドでオブジェクトを作成するより良い方法があるはずです! あるいは、別のスレッドなしでこれを行うことはできますか? 通常の .NET シリアル ポート クラスの内部実装の詳細について説明している msdn フォーラムのこの投稿に基づいて、この特定の設計を選択しました。

コメントから少し追加情報を更新します。

  • 問題のスレッドでは IsBackground が true に設定されています
  • 上記の管理されていないリソースは、問題には影響しません。例のすべてがマネージド リソースを使用していたとしても、同じ問題が発生します。
4

2 に答える 2

4

暗黙的な "This" パラメーターを取り除くために、ワーカー スレッド メソッドを少し変更し、"this" 参照をパラメーターとして渡しました。

public static void WorkerThreadMethod(object thisParameter)
{
  //Extract the things we need from the parameter passed in (the DataPort)
  //dataReady used to be 'this.dataReady' and closeSignal used to be
  //'this.closeSignal'
  ManualResetEvent dataReady = ((DataPort)thisParameter).dataReady;
  WaitHandle closeSignal = ((DataPort)thisParameter).closeSignal;

  thisParameter = null; //Forget the reference to the DataPort

  for(;;)
  {
    //Same as before, but without "this" . . .
  }
}

驚いたことに、これで問題は解決しませんでした。

SOS.dll に戻ると、ThreadHelper オブジェクトによって保持されている DataPort への参照がまだあることがわかりました。どうやら を実行してワーカースレッドをスピンアップするThread.Start(this);と、Start メソッドに渡した参照を保持するスレッドと同じ有効期間を持つプライベート ThreadHelper オブジェクトが作成されます (私は推測しています)。それは私たちに同じ問題を残します。何かが DataPort への参照を保持しています。これをもう一度試してみましょう。

//Code that starts the thread:
  Thread.Start(new WeakReference(this))
//. . .
public static void WorkerThreadMethod(object weakThisReference)
{
  DataPort strongThisReference= (DataPort)((WeakReference)weakThisReference).Target;

  //Extract the things we need from the parameter passed in (the DataPort)
  ManualResetEvent dataReady = strongThisReferencedataReady;
  WaitHandle closeSignal = strongThisReference.closeSignal;

  strongThisReference= null; //Forget the reference to the DataPort.

  for(;;)
  {
    //Same as before, but without "this" . . .
  }
}

これで大丈夫です。 作成された ThreadHelper は、ガベージ コレクションに影響を与えない WeakReference を保持します。ワーカー スレッドの開始時に必要なデータのみを DataPort から抽出し、DataPort へのすべての参照を意図的に失います。このアプリケーションではこれで問題ありません。これは、取得する部分が DataPort の存続期間にわたって変更されないためです。これで、最上位アプリケーションが DataPort へのすべての参照を失うと、ガベージ コレクションの対象になります。GC は、ワーカー スレッドを強制終了する Dispose メソッドを呼び出すファイナライザーを実行します。すべてが幸せです。

ただし、これを行うのは非常に面倒です (または、少なくとも正しく行う必要があります)。そのオブジェクトと同じ寿命を持つスレッドを所有するオブジェクトを作成するより良い方法はありますか? または、スレッドなしでこれを行う方法はありますか?

エピローグ: ほとんどの時間を WaitHandle.WaitAny() の実行に費やすスレッドを使用する代わりに、独自のスレッドを必要としないが、スレッドプールで継続を起動するある種の待機ハンドルを使用できれば素晴らしいでしょう。トリガーされたらスレッド。同様に、ハードウェア DLL が、イベントを通知する代わりに、新しいデータがあるたびにデリゲートを呼び出すことができたとしても、私はその DLL を制御しません。

于 2013-03-29T01:33:25.743 に答える
0

問題はあなたが示したコードではなく、このシリアル ポート ラッパー クラスを使用するコードにあると思います。そこに「using」ステートメントがない場合は、http://msdn.microsoft.com/en-us/library/yh598w02.aspxを参照してください。決定論的なクリーンアップ動作はありません。代わりに、ガベージ コレクターに依存しますが、まだ参照されているオブジェクトを取得することはなく、スレッドのすべてのスタック変数 (通常のパラメーターまたは this ポインターとして) が参照としてカウントされます。

于 2013-03-29T05:55:28.093 に答える