5

これとウェブ上の他の記事を読んだ後でも、最初のスレッドがロックに入った後、null を返す方法をまだ理解していません。それを理解している人は、私を助けて、より人間的な方法で説明してもらえますか?

「次のコードを考えてみてください。

public class Foo
{
  private static Foo instance;
  private static readonly object padlock = new object();

  public static Foo Get()
  {
    if (instance == null)
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Foo();
        }
      }
    }
    return instance;
  }
};

上記のコードを考えると、Foo インスタンスを初期化する書き込みは、インスタンス値の書き込みまで遅延する可能性があるため、インスタンスが初期化された状態でオブジェクトを返す可能性が生じます。

これを回避するには、インスタンス値を volatile にする必要があります。"

4

2 に答える 2

16

戻るnullことは問題ではありません。問題は、新しいインスタンスが、別のスレッドによって認識されるように、部分的に構築された状態になる可能性があることです。のこの宣言を考えてみましょうFoo

class Foo
{
  public int variable1;
  public int variable2;

  public Foo()
  {
    variable1 = 1;
    variable2 = 2;
  }
}

C# コンパイラ、JIT コンパイラ、またはハードウェアによってコードがどのように最適化されるかを次に示します。1

if (instance == null)
{
  lock (padlock)
  {
    if (instance == null)
    {
      instance = alloc Foo;
      instance.variable1 = 1; // inlined ctor
      instance.variable2 = 2; // inlined ctor
    }
  }
}
return instance;

まず、コンストラクターがインライン化されていることに注意してください (単純なため)。instanceこれで、構成フィールドがコンストラクター内で初期化される前に参照が割り当てられることを簡単に確認できることを願っています。論理フローの境界を通過しlockたり変更したりしない限り、読み取りと書き込みは自由に上下に移動できるため、これは有効な戦略です。彼らはしません。そのため、完全に初期化される前に、別のスレッドがそれを見て使用しようとする可能性があります。instance != null

volatileは、読み取りを取得フェンスとして扱い、書き込みを解放フェンスとして扱うため、この問題を修正します。

  • acquire-fence: 他の読み取りと書き込みがフェンスの前に移動できないメモリ バリア。
  • release-fence: 他の読み取りと書き込みがフェンスの後に移動できないメモリ バリア。

そのため、次のようにマークinstanceするとvolatile、リリース フェンスによって上記の最適化が妨げられます。バリア アノテーションを使用したコードは次のようになります。リリース フェンスを示すために ↑ 矢印を使用し、取得フェンスを示すために ↓ 矢印を使用しました。↑ 矢印を超えて下に移動したり、↓ 矢印を超えて上に移動したりすることは許可されていないことに注意してください。矢頭はすべてを押しのけるものと考えてください。

var local = instance;
↓ // volatile read barrier
if (local == null)
{
  var lockread = padlock;
  ↑ // lock full barrier
  lock (lockread)
  ↓ // lock full barrier
  {
    local = instance;
    ↓ // volatile read barrier
    if (local == null)
    {
      var ref = alloc Foo;
      ref.variable1 = 1; // inlined ctor
      ref.variable2 = 2; // inlined ctor
      ↑ // volatile write barrier
      instance = ref;
    }
  ↑ // lock full barrier
  }
  ↓ // lock full barrier
}
local = instance;
↓ // volatile read barrier
return local;

の構成変数への書き込みFooは引き続き並べ替えることができますが、メモリ バリアにより、 への代入後に書き込みが行われないことに注意してinstanceください。矢印をガイドとして使用して、許可されているさまざまな最適化戦略と許可されていないさまざまな最適化戦略を想像してください。↑ 矢印を超えて下に移動したり、↓ 矢印を超えて上に移動したりすることは許可されていないことに注意してください。

Thread.VolatileWriteこの問題も解決され、volatileVB.NET のようなキーワードのない言語でも使用できます。がどのようVolatileWriteに実装されているかを見ると、これがわかります。

public static void VolatileWrite(ref object address, object value)
{
  Thread.MemoryBarrier();
  address = value;
}

これは、最初は直感に反するように思えるかもしれません。結局、メモリバリアは割り当てのに配置されます。あなたが求めるメインメモリにコミットされた割り当てを取得するのはどうですか? 割り当ての後にバリアを配置する方が正しいのではないでしょうか? それがあなたの直感があなたに言っていることなら、それは間違っています。メモリバリアは、厳密には「新鮮な読み取り」または「コミットされた書き込み」を取得することではありません。それはすべて、命令の順序付けに関するものです。これは、私が目にする混乱の最大の原因です。

Thread.MemoryBarrierが実際にフル フェンス バリアを生成することにも言及することが重要かもしれません。したがって、上記の表記を矢印で使用すると、次のようになります。

public static void VolatileWrite(ref object address, object value)
{
  ↑ // full barrier
  ↓ // full barrier
  address = value;
}

したがって、技術的には、呼び出しはフィールドVolatileWriteへの書き込みよりも多くのことを行います。たとえば、VB.NET では許可されていませんがvolatile、BCL の一部であるため、他の言語で使用できることに注意してください。volatileVolatileWrite


1この最適化はほとんど理論上のものです。ECMA 仕様では技術的には許可されていますが、ECMA 仕様の Microsoft CLI 実装では、すべての書き込みがリリース フェンス セマンティクスを既に持っているかのように扱われます。ただし、CLI の別の実装でもこの最適化を実行できる可能性があります。

于 2012-04-23T13:29:38.057 に答える
3

Bill Pughは、このテーマに関するいくつかの記事を執筆しており、このトピックに関するリファレンスです。

注目すべき参考資料は、「二重チェックされたロックが壊れている」という宣言です。

大まかに言えば、ここに問題があります:

マルチコアVMでは、同期バリア(またはメモリフェンス)に到達するまで、スレッドによる書き込みが他のスレッドに表示されない場合があります。「メモリバリア:ソフトウェアハッカーのためのハードウェアビュー」を読むことができます。これは、この問題に関する非常に優れた記事です。

したがって、スレッドAが1つのフィールドでオブジェクトを初期化し、aそのオブジェクトの参照をref別のオブジェクトのフィールドに格納する場合B、メモリには2つの「セル」がaありrefます。スレッドがメモリフェンスで変更の可視性を強制しない限り、両方のメモリ位置への変更が同時に他のスレッドに表示されない場合があります。

Javaでは、同期を強制することができますsynchronized。これはコストがかかり、代わりにフィールドを宣言しますvolatile。この場合、このセルへの変更は常にすべてのスレッドに表示されます。

ただし、揮発性のセマンティクスはJava 4と5の間で変化します。Java4では、前述の例でdoulbeチェックを機能させるために、aと揮発性の両方を定義する必要があります。ref

それは直感的ではなく、ほとんどの人はref揮発性として設定するだけでした。したがって、これを変更し、Java 5以降では、揮発性フィールドが変更されると(ref)、変更された他のフィールドの同期がトリガーされます(a)。

編集:JavaではなくC#を要求しているのがわかります...それでも役立つかもしれないので、答えは残しておきます。

于 2012-04-23T13:09:42.753 に答える