概要: 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 で問題を再現できませんでした。