25

MSDN Magazineの記事では、Read Introductionの概念について説明し、それによって壊れる可能性のあるコードサンプルを示しています。

public class ReadIntro {
  private Object _obj = new Object();
  void PrintObj() {
    Object obj = _obj;
    if (obj != null) {
      Console.WriteLine(obj.ToString()); // May throw a NullReferenceException
    }
  }
  void Uninitialize() {
    _obj = null;
  }
}

この「NullReferenceExceptionをスローする可能性があります」というコメントに注意してください。これが可能かどうかはわかりませんでした。

だから私の質問は:どうすれば読まれた紹介から保護できますか?

また、記事には読み取りが含まれていないため、コンパイラが読み取りを導入することを決定した正確な説明にも感謝します。

4

3 に答える 3

19

この複雑な質問を分解して明確にしようと思います。

「紹介を読む」とは何ですか?

「はじめに読む」は、コードが次のように最適化されたものです。

public static Foo foo; // I can be changed on another thread!
void DoBar() {
  Foo fooLocal = foo;
  if (fooLocal != null) fooLocal.Bar();
}

ローカル変数を削除することで最適化されます。コンパイラーは、スレッドが1つしかない場合foofooLocalは同じであると推論できます。コンパイラは、マルチスレッドシナリオで表示されるようになった場合でも、シングルスレッドでは表示されない最適化を行うことを明示的に許可されています。したがって、コンパイラはこれを次のように書き換えることができます。

void DoBar() {
  if (foo != null) foo.Bar();
}

そして今、競合状態があります。fooチェック後にnull以外からnullに変わった場合はfoo、2回目に読み取られ、2回目にnullになる可能性があり、クラッシュする可能性があります。クラッシュダンプを診断する人の観点からすると、これは完全に不思議です。

これは実際に起こり得ますか?

あなたがリンクした記事が呼びかけたように:

x86-x64上の.NETFramework4.5でこのコードサンプルを使用して、NullReferenceExceptionを再現することはできないことに注意してください。.NET Framework 4.5でリードイントロダクションを再現することは非常に困難ですが、それでも特定の特別な状況で発生します。

x86 / x64チップには「強力な」メモリモデルがあり、jitコンパイラはこの分野では積極的ではありません。彼らはこの最適化を行いません。

ARMチップなどの弱いメモリモデルプロセッサでコードを実行している場合は、すべての賭けが無効になります。

「コンパイラ」と言うとき、どのコンパイラを意味しますか?

私はjitコンパイラを意味します。C#コンパイラは、この方法で読み取りを導入することはありません。(許可されていますが、実際には許可されていません。)

メモリバリアなしでスレッド間でメモリを共有することは悪い習慣ではありませんか?

はい。の値はfooすでにプロセッサキャッシュにキャッシュされた古い値である可能性があるため、ここでメモリバリアを導入するために何かを行う必要があります。メモリバリアを導入するための私の好みは、ロックを使用することです。フィールドを作成するvolatileか、を使用するVolatileReadか、またはいずれかのInterlocked方法を使用することもできます。それらはすべてメモリバリアをもたらします。(volatile参考までに「ハーフフェンス」のみを紹介します。)

メモリバリアがあるからといって、必ずしも読み取り導入の最適化が実行されないことを意味するわけではありません。ただし、メモリバリアを含むコードに影響を与える最適化の追求については、ジッターはそれほど積極的ではありません。

このパターンに他の危険はありますか?

もちろん!読まれた紹介がないと仮定しましょう。まだ競合状態がありますfooチェック後に別のスレッドがnullに設定され、消費するグローバル状態も変更しBarた場合はどうなりますか?これで2つのスレッドができました。1つfooはnullではなくグローバル状態はへの呼び出しに問題がないと考えており、もう1Barつは反対のスレッドを信じて実行していますBar。これは災害のレシピです。

では、ここでのベストプラクティスは何ですか?

まず、スレッド間でメモリを共有しないでください。プログラムのメインライン内に2つの制御スレッドがあるというこの全体的な考えは、そもそもクレイジーです。そもそもそうあるべきではなかった。スレッドを軽量プロセスとして使用します。プログラムのメインラインのメモリとまったく相互作用しない独立したタスクを実行するように与え、それらを使用して計算量の多い作業を実行します。

次に、スレッド間でメモリを共有する場合は、ロックを使用してそのメモリへのアクセスをシリアル化します。ロックが競合しない場合は安価であり、競合がある場合は、その問題を修正します。ローロックおよびノー​​ロックのソリューションは、正しく理解するのが難しいことで有名です。

第3に、スレッド間でメモリを共有する場合、その共有メモリを含む呼び出し元のすべてのメソッドは、競合状態に直面しても堅牢であるか、競合を排除する必要があります。それは負担が大きいので、そもそもそこに行くべきではありません。

私のポイントは、イントロダクションを読むのは怖いですが、率直に言って、スレッド間でメモリを簡単に共有するコードを書いている場合は、心配する必要はほとんどありません。最初に心配することは千と他にあります。

于 2013-02-11T16:18:04.777 に答える
8

これはコンパイラの最適化であるため、読み取りの導入から実際に「保護」することはできません(もちろん、最適化なしでデバッグビルドを使用する場合を除きます)。オプティマイザーが関数のシングルスレッドセマンティクスを維持することはかなりよく文書化されています。これは、記事の注記にあるように、マルチスレッドの状況で問題を引き起こす可能性があります。

そうは言っても、私は彼の例に混乱しています。JeffreyRichterの本CLRviaC#(この場合はv3)で、彼はこのパターンをカバーし、上記のスニペットの例では、理論的には機能しないことに注意しています。しかし、これは.Netの存在の初期にMicrosoftが推奨したパターンであったため、彼が話したJITコンパイラの人々は、その種のスニペットが壊れないようにする必要があると述べました。(ただし、何らかの理由で壊す価値があると彼らが判断する可能性は常にあります。EricLippertがそれに光を当てることができると思います)。

最後に、記事とは異なり、Jeffreyは、マルチスレッドの状況でこれを処理するための「適切な」方法を提供します(サンプルコードで彼の例を変更しました)。

Object temp = Interlocked.CompareExchange(ref _obj, null, null);
if(temp != null)
{
    Console.WriteLine(temp.ToString());
}
于 2013-02-10T17:20:18.930 に答える
1

私は記事をざっと読んだだけですが、著者が探しているのは、_objメンバーをとして宣言する必要があるということのようvolatileです。

于 2013-02-10T17:39:09.863 に答える