「新鮮」が何を意味するかによります。Thread.MemoryBarrier
指定されたメモリ位置から変数をロードすることによって、変数の最初の読み取りを強制的に取得します。それが「フレッシュ」の意味するすべてであり、それ以上のことではない場合、答えはイエスです. ほとんどのプログラマーは、認識しているかどうかにかかわらず、より厳格な定義で動作しており、そこから問題と混乱が始まります。揮発性読み取り経由volatile
および他の同様のメカニズムは、この定義では「新鮮な」読み取りを生成しませんが、別の定義では生成することに注意してください。読み続けて、その方法を見つけてください。
下向きの矢印 ↓ を使用して揮発性の読み取りを表し、上向きの矢印 ↑ を使用して揮発性の書き込みを表します。矢印の頭は、他の読み取りと書き込みを押しのけるものと考えてください。これらのメモリ フェンスを生成するコードは、命令が下向き矢印で上に移動し、上向き矢印で下に移動しない限り、自由に動き回ることができます。ただし、メモリ フェンス (矢印) は、コード内で最初に宣言された場所でロックされています。Thread.MemoryBarrier
フルフェンスバリアを生成するため、読み取り-取得と解放-書き込みの両方のセマンティクスがあります。
int a = 0;
int b = 0;
void A() // runs in thread A
{
register = 1
a = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = b
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
b = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = a
jump Console.WriteLine
use register
return Console.WriteLine
}
C# の行は、JIT をコンパイルして実行すると、実際にはマルチパート命令になることに注意してください。私はそれをいくらか説明しようとしましたが、実際には の呼び出しはConsole.WriteLine
示されているよりもはるかに複雑になるため、a
orの読み取りb
とそれらの最初の使用の間の時間は、相対的に言えば重要になる可能性があります。取得フェンスを生成するためThread.MemoryBarrier
、読み取りがフロートアップして呼び出しを通過することは許可されません。したがって、読み取りはThread.MemoryBarrier
呼び出しに対して「新鮮」です。ただし、Console.WriteLine
呼び出しで実際に使用された時点に比べて「古く」なる可能性があります。
Thread.MemoryBarrier
呼び出しをvolatile
キーワードに置き換えた場合、コードがどのようになるかを考えてみましょう。
volatile int a = 0;
volatile int b = 0;
void A() // runs in thread A
{
register = 1
↑ // volatile write
a = register
register = b
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
↑ // volatile write
b = register
register = a
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
変化がわかりますか?まばたきをしたら、見逃したことになります。2 つのコード ブロック間の矢印 (メモリ フェンス) の配置を比較します。最初のケース ( Thread.MemoryBarrier
) では、メモリバリアより前の時点での読み取りは許可されません。ただし、2 番目のケース ( volatile
) では、読み取りが無期限にバブルアップする可能性があります (下向きの矢印がそれらを押しのけるため)。この場合Thread.MemoryBarrier
、解決策よりも読み取りの前に配置すると、「より新鮮な」読み取りを生成できる合理的な議論を行うことができvolatile
ます。しかし、その読みが「新鮮」であると主張できますか? それが使用されるConsole.WriteLine
までには、最新の値ではない可能性があるため、そうではありません。
それで、volatile
あなたが尋ねるかもしれない使用のポイントは何ですか。連続した読み取りはフェンス取得セマンティクスを生成するため、後の読み取りが前の読み取りよりも新しい値を生成することが保証されます。次のコードを検討してください。
volatile int a = 0;
void A()
{
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
}
ここで起こり得ることに細心の注意を払ってください。線register = a
は読み取りを表します。↓の矢印の位置に注目してください。読み取りの後に配置されるため、実際の読み取りが浮き上がるのを妨げるものは何もありません。実際には、前のConsole.WriteLine
呼び出しの前に浮き上がる可能性があります。したがって、この場合、 がConsole.WriteLine
の最新の値で動作するという保証はありませんa
。ただし、最後に呼び出されたときよりも新しい値で動作することが保証されています。それは一言で言えばその有用性です。そのため、意図した操作が成功したと仮定する前に、揮発性変数の以前の読み取りが現在の読み取りと等しいことを確認するために、while ループでスピンする多くのロックフリー コードが表示されます。
結論として、いくつかの重要な点を述べたいと思います。
Thread.MemoryBarrier
は、バリアに関連する最新の値を返す後に表示される読み取りを保証します。しかし、実際に意思決定を行ったり、その情報を使用したりする頃には、最新の値ではなくなっている可能性があります。
volatile
読み取りが、同じ変数の前回の読み取りよりも新しい値を返すことを保証します。ただし、値が最新であることを保証するものではありません。
- 「フレッシュ」の意味は明確に定義する必要がありますが、状況や開発者によって異なる場合があります。形式的に定義され明確化されている限り、これ以上正しいという意味はありません。
- 絶対的な概念ではありません。メモリ バリアや以前の命令の生成など、他の何かに関連するという観点から「フレッシュ」を定義する方が便利であることがわかります。つまり「鮮度」とは、アインシュタインの特殊相対性理論で速度が観測者に対してどのように相対的であるかのような相対的な概念です。