戻る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
この問題も解決され、volatile
VB.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 の一部であるため、他の言語で使用できることに注意してください。volatile
VolatileWrite
1この最適化はほとんど理論上のものです。ECMA 仕様では技術的には許可されていますが、ECMA 仕様の Microsoft CLI 実装では、すべての書き込みがリリース フェンス セマンティクスを既に持っているかのように扱われます。ただし、CLI の別の実装でもこの最適化を実行できる可能性があります。