4

C# に次のコードがあるとします。

int a = 0;
int b = 0;

void A() // runs in thread A
{
    a = 1;
    Thread.MemoryBarrier();
    Console.WriteLine(b);
}

void B() // runs in thread B
{
    b = 1;
    Thread.MemoryBarrier();
    Console.WriteLine(a);
}

MemoryBarriers書き込み命令が読み取りの前に行われることを確認してください。ただし、あるスレッドの書き込みが他のスレッドの読み取りによって認識されることが保証されていますか? 言い換えれば、少なくとも 1 つのスレッドが印刷される、1または両方のスレッドが印刷されることが保証されているの0でしょうか?

thisthisのように、「鮮度」やMemoryBarrierC# に関連するいくつかの質問が既に存在することを知っています。ただし、それらのほとんどは、書き込み解放および読み取り取得パターンを扱っています。この質問に投稿されたコードは、命令が順番に保持されているという事実に加えて、読み取りによって書き込みが表示されることが保証されているかどうかに非常に固有のものです。

4

3 に答える 3

3

「新鮮」が何を意味するかによります。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示されているよりもはるかに複雑になるため、aorの読み取り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読み取りが、同じ変数の前回の読み取りよりも新しい値を返すことを保証します。ただし、値が最新であることを保証するものではありません。
  • 「フレッシュ」の意味は明確に定義する必要がありますが、状況や開発者によって異なる場合があります。形式的に定義され明確化されている限り、これ以上正しいという意味はありません。
  • 絶対的な概念ではありません。メモリ バリアや以前の命令の生成など、他の何かに関連するという観点から「フレッシュ」を定義する方が便利であることがわかります。つまり「鮮度」とは、アインシュタインの特殊相対性理論で速度が観測者に対してどのように相対的であるかのような相対的な概念です。
于 2016-10-25T19:30:07.953 に答える
3

両方のスレッドが書き込みを行うことは保証されていません1。次のルールに基づいて、読み取り/書き込み操作の順序のみが保証されます。

現在のスレッドを実行しているプロセッサは、呼び出しの前のメモリ アクセスが への呼び出し続くMemoryBarrierメモリ アクセスの後に実行されるように命令を並べ替えることができません。MemoryBarrier

したがって、これは基本的に、a のスレッドがバリアの呼び出しのに読み取らthread Aれた変数の値を使用しないことを意味します。ただし、コードが次のような場合は、値をキャッシュします。b

void A() // runs in thread A
{
    a = 1;
    Thread.MemoryBarrier();
    // b may be cached here
    // some work here
    // b is changed by other thread
    // old value of b is being written
    Console.WriteLine(b);
}

並列実行の競合状態のバグは再現が非常に難しいため、上記のシナリオを確実に実行するコードを提供することはできませんが、異なるスレッドで使用される変数にvolatileキーワードを使用することをお勧めします。必要に応じて正確に機能します-変数の新しい読み取りを提供します:

volatile int a = 0;
volatile int b = 0;

void A() // runs in thread A
{
    a = 1;
    Thread.MemoryBarrier();
    Console.WriteLine(b);
}

void B() // runs in thread B
{
    b = 1;
    Thread.MemoryBarrier();
    Console.WriteLine(a);
}
于 2016-06-27T12:25:09.737 に答える