5

概要: C#/.NET はガベージ コレクションの対象になるはずです。C# には、リソースをクリーンアップするために使用されるデストラクタがあります。オブジェクト A が、その変数メンバーの 1 つを複製しようとしたのと同じ行でガベージ コレクションされるとどうなりますか? どうやら、マルチプロセッサでは、ガベージコレクタが勝つことがあります...

問題

今日、C# のトレーニング セッションで、先生は、マルチプロセッサで実行した場合にのみバグを含むコードをいくつか見せてくれました。

要約すると、呼び出されたメソッドから戻る前に C# クラス オブジェクトのファイナライザーを呼び出して、コンパイラーまたは JIT が失敗することがあります。

Visual C++ 2005 ドキュメントに記載されている完全なコードは、非常に大きな質問を避けるために「回答」として投稿されますが、重要なものは以下のとおりです。

次のクラスには、内部配列の複製コピーを返す「ハッシュ」プロパティがあります。この構築では、配列の最初の項目の値は 2 です。デストラクタでは、その値はゼロに設定されます。

ポイントは次のとおりです。「例」の「ハッシュ」プロパティを取得しようとすると、オブジェクトが使用されているため (そのため、使用されていないため、最初の項目がまだ 2 である配列のクリーンなコピーが取得されます)。ガベージ コレクション/ファイナライズ済み):

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

しかし、これほど単純なことはありません... このクラスを使用するコードはスレッド内で実行されます。もちろん、テストのために、アプリは高度にマルチスレッド化されています。

public static void Main(string[] args)
{
    Thread t = new Thread(new ThreadStart(ThreadProc));
    t.Start();
    t.Join();
}

private static void ThreadProc()
{
    // running is a boolean which is always true until
    // the user press ENTER
    while (running) DoWork();
}

DoWork 静的メソッドは、問題が発生するコードです。

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2)
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
}

DoWork の 1,000,000 回の実行ごとに、明らかに、ガベージ コレクターはその魔法を実行し、関数の remaning コードで参照されなくなったため、"ex" を再利用しようとします。今回は、"Hash" よりも高速です。メソッドを取得します。したがって、最終的に得られるのは、正しいバイト配列 (最初の項目が 2 の場合) ではなく、ゼロで埋められたバイト配列のクローンです。

私の推測では、コードのインライン化があり、DoWork 関数で [1] とマークされた行が次のように置き換えられます。

    // Supposed inlined processing
    byte[] res2 = ex.Hash2;
    // note that after this line, "ex" could be garbage collected,
    // but not res2
    byte[] res = (byte[])res2.Clone();

Hash2 が次のようにコード化された単純なアクセサであると仮定した場合:

// Hash2 code:
public byte[] Hash2 { get { return (byte[])hashValue; } }

問題は、これは C#/.NET でそのように動作するはずなのか、それとも JIT のコンパイラのバグと見なすことができるのかということです。

編集

説明については、Chris Brumme と Chris Lyons のブログを参照してください。

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/2004/09/21/232445.aspx

皆さんの回答は面白かったのですが、どれも良いとは言えませんでした。だから私はあなたに+1を与えました...

ごめん

:-)

編集 2

同じ条件 (複数の同じ実行可能ファイルを同時に実行する、リリース モードなど) で同じコードを使用したにもかかわらず、Linux/Ubuntu/Mono で問題を再現できませんでした。

4

8 に答える 8

9

これは単なるコードのバグです。ファイナライザーはマネージド オブジェクトにアクセスすべきではありません。

ファイナライザーを実装する唯一の理由は、アンマネージ リソースを解放することです。この場合、標準の IDisposable パターンを慎重に実装する必要があります。

このパターンでは、保護されたメソッド「protected Dispose(bool disposing)」を実装します。このメソッドがファイナライザーから呼び出されると、アンマネージド リソースはクリーンアップされますが、マネージド リソースはクリーンアップされません。

あなたの例では、管理されていないリソースがないため、ファイナライザーを実装しないでください。

于 2008-09-27T12:53:23.077 に答える
3

あなたが見ているものは完全に自然です。

バイト配列を所有するオブジェクトへの参照を保持しないため、オブジェクト(バイト配列ではない)は実際にはガベージコレクターが収集するために自由になります。

ガベージコレクターは本当に攻撃的である可能性があります。

したがって、内部データ構造への参照を返すオブジェクトのメソッドを呼び出し、オブジェクトのファイナライザーがそのデータ構造を台無しにする場合は、オブジェクトへのライブ参照も保持する必要があります。

ガベージコレクターは、ex変数がそのメソッドで使用されなくなったことを確認するため、適切な状況(つまり、タイミングと必要性)でガベージコレクションを実行できます。

これを行う正しい方法は、exでGC.KeepAliveを呼び出すことです。したがって、このコード行をメソッドの最後に追加すると、すべてうまくいくはずです。

GC.KeepAlive(ex);

JeffreyRichterの著書Applied.NETFramework Programmingを読んで、この攻撃的な動作について学びました。

于 2008-09-25T17:37:01.767 に答える
1

ex.Hash呼び出しの後、CLRは、exインスタンスがもう必要ないことを知っているので、これは、doworkメソッドでファイナライザーが呼び出されるのは完全に正常です...

ここで、インスタンスを存続させたい場合は、次のようにします。

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2) // NOTE
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
  GC.KeepAlive(ex); // keep our instance alive in case we need it.. uh.. we don't
}

GC.KeepAliveは...何もしません:)これは空のinlinable/jittableメソッドであり、その唯一の目的は、GCをだましてオブジェクトがこの後に使用されると思わせることです。

警告:DoWorkメソッドがマネージC ++メソッドである場合、この例は完全に有効です...別のスレッド内からデストラクタを呼び出さないようにする場合は、マネージインスタンスを手動で維持する必要があります。IE。ファイナライズ時にアンマネージメモリのBLOBを削除しようとしている管理対象オブジェクトへの参照を渡し、メソッドはこれと同じBLOBを使用します。インスタンスを存続させないと、GCとメソッドのスレッドの間で競合状態が発生します。

そして、これは涙に終わるでしょう。そして、管理されたヒープの破損...

于 2008-09-25T17:36:27.493 に答える
1

Chris Brumme のブログからの興味深いコメント

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx

class C {<br>
   IntPtr _handle;
   Static void OperateOnHandle(IntPtr h) { ... }
   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();
         aC.m();
         ...  // most guess here
      } else {
         ...
      }
   }
}

したがって、上記のコードで 'aC' がどれくらい存続するかはわかりません。JIT は、Other.work() が完了するまで参照を報告する場合があります。Other.work() を他のメソッドにインライン化し、aC をさらに長くレポートする場合があります。「aC = null;」を追加しても それを使用した後、JIT はこの割り当てをデッド コードと見なして自由に削除できます。JIT が参照の報告をいつ停止したかに関係なく、GC はしばらくの間、参照を収集できない可能性があります。

aC を収集できる最も早い時点について心配する方が興味深いです。ほとんどの人と同じように、aC がコレクションの対象になるのが最も早いのは、Other.work() の「if」句の右中括弧 (ここにコメントを追加した場所) であると推測するでしょう。実際、中かっこは IL には存在しません。これらは、ユーザーと言語コンパイラーの間の構文上の契約です。 Other.work() は、aC.m() の呼び出しを開始するとすぐに aC の報告を自由に停止できます。

于 2008-09-25T19:53:07.057 に答える
1

これは、作業スレッドと GC スレッドの間の競合状態のようです。それを避けるために、私は2つのオプションがあると思います:

(1) if ステートメントを res の代わりに ex.Hash[0] を使用するように変更して、ex が時期尚早に GC されないようにする、または

(2) Hash の呼び出し中に ex をロックする

これは非常に気の利いた例ですが、マルチコア システムでのみ現れる JIT コンパイラにバグがある可能性がある、またはこの種のコーディングではガベージ コレクションで微妙な競合状態が発生する可能性があるという先生の指摘はありましたか?

于 2008-09-25T17:27:29.590 に答える
1

物事が複数のスレッドで実行されているという事実により、あなたが見ているのは合理的な動作だと思います。これが GC.KeepAlive() メソッドの理由です。この場合、オブジェクトがまだ使用されており、クリーンアップの候補ではないことを GC に伝えるために使用する必要があります。

「完全なコード」応答の DoWork 関数を見ると、問題は次のコード行の直後にあります。

byte[] res = ex.Hash;

関数はexオブジェクトを参照しなくなるため、その時点でガベージ コレクションの対象になります。GC.KeepAlive への呼び出しを追加すると、これが発生しなくなります。

于 2008-09-25T17:29:29.540 に答える
1

はい、これは以前から出ていた問題です。

これを行うには release を実行する必要があり、「えっ、どうしてそれが null になるの?」と頭を悩ませてしまうという点で、さらに楽しいです。

于 2008-09-25T18:46:01.447 に答える
0

完全なコード

以下は、Visual C++ 2008 .cs ファイルからコピー/貼り付けされた完全なコードです。私は現在 Linux を使用しており、Mono コンパイラやその使用に関する知識がないため、今すぐテストを行う方法はありません。それでも、数時間前に、このコードが機能し、バグがあることを確認しました。

using System;
using System.Threading;

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }
    public byte[] Hash2 { get { return (byte[])hashValue; } }

    public int returnNothing() { return 25; }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

public class Test
{
    private static int totalCount = 0;
    private static int finalizerFirstCount = 0;

    // This variable controls the thread that runs the demo.
    private static bool running = true;

    // In order to demonstrate the finalizer running first, the
    // DoWork method must create an Example object and invoke its
    // Hash property. If there are no other calls to members of
    // the Example object in DoWork, garbage collection reclaims
    // the Example object aggressively. Sometimes this means that
    // the finalizer runs before the call to the Hash property
    // completes. 

    private static void DoWork()
    {
        totalCount++;

        // Create an Example object and save the value of the 
        // Hash property. There are no more calls to members of 
        // the object in the DoWork method, so it is available
        // for aggressive garbage collection.

        Example ex = new Example();

        // Normal processing
        byte[] res = ex.Hash;

        // Supposed inlined processing
        //byte[] res2 = ex.Hash2;
        //byte[] res = (byte[])res2.Clone();

        // successful try to keep reference alive
        //ex.returnNothing();

        // Failed try to keep reference alive
        //ex = null;

        // If the finalizer runs before the call to the Hash 
        // property completes, the hashValue array might be
        // cleared before the property value is read. The 
        // following test detects that.

        if (res[0] != 2)
        {
            finalizerFirstCount++;
            Console.WriteLine("The finalizer ran first at {0} iterations.", totalCount);
        }

        //GC.KeepAlive(ex);
    }

    public static void Main(string[] args)
    {
        Console.WriteLine("Test:");

        // Create a thread to run the test.
        Thread t = new Thread(new ThreadStart(ThreadProc));
        t.Start();

        // The thread runs until Enter is pressed.
        Console.WriteLine("Press Enter to stop the program.");
        Console.ReadLine();

        running = false;

        // Wait for the thread to end.
        t.Join();

        Console.WriteLine("{0} iterations total; the finalizer ran first {1} times.", totalCount, finalizerFirstCount);
    }

    private static void ThreadProc()
    {
        while (running) DoWork();
    }
}

興味のある方には、圧縮したプロジェクトを電子メールで送信できます。

于 2008-09-25T17:24:11.050 に答える