あなたが言及している言葉遣いは、私がよく使う言葉のように見えます。ただし、仕様には次のように記載されています。
- 揮発性フィールドの読み取りは、揮発性読み取りと呼ばれます。揮発性読み取りには「取得セマンティクス」があります。つまり、命令シーケンスでメモリへの参照の後に発生するメモリへの参照の前に発生することが保証されています。
- 揮発性フィールドの書き込みは、揮発性書き込みと呼ばれます。揮発性書き込みには「リリースセマンティクス」があります。つまり、命令シーケンスの書き込み命令の前のメモリ参照の後に発生することが保証されています。
ただし、指示を移動できることに焦点を当てたいので、私は通常、質問で引用した表現を使用します。あなたが引用した言葉遣いと仕様は同等です。
いくつかの例を紹介します。これらの例では、リリースフェンスを示すために↑矢印を使用し、取得フェンスを示すために↓矢印を使用する特別な表記法を使用します。他の命令は、↑矢印を超えて下にフロートしたり、↓矢印を超えて上にフロートしたりすることはできません。鏃はそれからすべてをはじくものと考えてください。
次のコードを検討してください。
static int x = 0;
static int y = 0;
static void Main()
{
x++
y++;
}
個々の指示を表示するように書き直すと、次のようになります。
static void Main()
{
read x into register1
increment register1
write register1 into x
read y into register1
increment register1
write register1 into y
}
この例にはメモリバリアがないため、実行中のスレッドによって認識される論理シーケンスが物理シーケンスと一致している限り、C#コンパイラ、JITコンパイラ、またはハードウェアはさまざまな方法で自由に最適化できます。これがそのような最適化の1つです。読み取りと書き込みがどのように行われ、スワップされたかに注意してx
くださいy
。
static void Main()
{
read y into register1
read x into register2
increment register1
increment register2
write register1 into y
write register2 into x
}
今回は、これらの変数をに変更しますvolatile
。矢印表記を使用して、メモリバリアをマークします。読み取りと書き込みの順序がどのように保持されているかに注意してx
くださいy
。これは、指示がバリア(↓および↑の矢印で示されている)を超えて移動できないためです。さて、これは重要です。命令のインクリメントと書き込みx
は引き続きフロートダウンし、読み取りはy
フロートアップできることに注意してください。ハーフフェンスを使用していたため、これは引き続き有効です。
static volatile int x = 0;
static volatile int y = 0;
static void Main()
{
read x into register1
↓ // volatile read
read y into register2
↓ // volatile read
increment register1
increment register2
↑ // volatile write
write register1 into x
↑ // volatile write
write register2 into y
}
これは非常に簡単な例です。ダブルチェックされたパターンにどのように違いをもたらすことができるかについての重要な例については、ここで私の答えを見てください。volatile
ここで使用したのと同じ矢印表記を使用して、何が起こっているかを簡単に視覚化できるようにします。
これで、操作する方法もありますThread.MemoryBarrier
。フルフェンスを生成します。したがって、矢印表記を使用すると、それがどのように機能するかを視覚化できます。
この例を考えてみましょう。
static int x = 0;
static int y = 0;
static void Main
{
x++;
Thread.MemoryBarrier();
y++;
}
以前のように個々の指示を表示する場合は、次のようになります。現在、命令の移動が完全に防止されていることに注意してください。命令の論理シーケンスを損なうことなくこれを実行できる方法は他にありません。
static void Main()
{
read x into register1
increment register1
write register1 into x
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
read y into register1
increment register1
write register1 into y
}
さて、もう1つの例。今回はVB.NETを使用しましょう。volatile
VB.NETにはキーワードがありません。では、VB.NETで揮発性の読み取りをどのように模倣できますか?を使用しますThread.MemoryBarrier
。1
Public Function VolatileRead(ByRef address as Integer) as Integer
Dim local = address
Thread.MemoryBarrier()
Return local
End Function
そして、これは私たちの矢印表記でどのように見えるかです。
Public Function VolatileRead(ByRef address as Integer) as Integer
read address into register1
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
return register1
End Function
揮発性の読み取りを模倣したいので、実際の読み取りの後Thread.MemoryBarrier
に呼び出しを行う必要があることに注意することが重要です。揮発性の読み取りは「新規読み取り」を意味し、揮発性の書き込みは「コミットされた書き込み」を意味するという考えに陥らないでください。それはそれがどのように機能するかではなく、それは確かに仕様が説明しているものではありません。
アップデート:
画像を参考に。
待つ!すべての書き込みが終了したことを確認しています。
と
待つ!私はすべての消費者が現在の価値を持っていることを確認しています!
これは私が話していた罠です。ステートメントは完全に正確ではありません。はい、ハードウェアレベルで実装されたメモリバリアは、キャッシュコヒーレンシラインを同期する可能性があり、その結果、上記のステートメントは、何が起こっているかをある程度正確に把握している可能性があります。しかし、volatile
指示の移動を制限するだけです。仕様では、メモリから値をロードしたり、メモリバリアが配置されている場所でメモリに値を保存したりすることについては何も述べていません。
1もちろん、Thread.VolatileRead
すでにビルトインがあります。そして、あなたはそれが私がここで行ったのとまったく同じように実装されていることに気付くでしょう。