最初のケースでは、バリア 1 が確実_answer
に BEFORE に書き込まれ_complete
ます。コードの記述方法や、コンパイラまたは CLR が CPU に指示する方法に関係なく、メモリ バスの読み取り/書き込みキューは要求を並べ替えることができます。バリアは基本的に「続行する前にキューをフラッシュする」と言います。同様に、Barrier 4 は_answer
AFTER が読み取られることを確認します_complete
。そうしないと、CPU2 が物事を並べ替えて、古いものを_answer
「新しい」ものと見なす可能性があります_complete
。
バリア 2 と 3 は、ある意味では役に立ちません。説明に「後」という言葉が含まれていることに注意してください。つまり、「... B が A の後に実行された場合、...」です。BがAを追うとはどういう意味ですか? B と A が同じ CPU 上にある場合、確かに、B は後にすることができます。ただし、その場合、同じ CPU はメモリ バリアの問題がないことを意味します。
したがって、B と A が異なる CPU で実行されているとします。さて、アインシュタインの相対性理論と同じように、異なる場所/CPU で時間を比較するという概念はあまり意味がありません。別の考え方として、B が A の後に走ったかどうかを判断できるコードを記述できますか? もしそうなら、おそらくそれを行うためにメモリバリアを使用しました。そうでなければ、あなたには分からないし、聞いても意味がありません。これはハイゼンブルグの原理にも似ています。これを観察できれば、実験を修正したことになります。
しかし、物理学はさておき、マシンのフードを開けて、 の実際のメモリ位置が true であることを確認できるとしましょう (A が実行されたため)。_complete
ここで B を実行します。Barrier 3 なしで、CPU2 はまだ_complete
true と見なさない可能性があります。つまり、「新鮮」ではありません。
しかし、おそらくマシンを開いて を見ることはできません_complete
。調査結果を CPU2 の B に伝えることもありません。唯一の通信は、CPU 自体が行っていることです。したがって、バリアなしで BEFORE/AFTER を判断できない場合、「B がバリアなしで A の後に実行された場合、B はどうなるか」と尋ねることは意味がありません。
ところで、C# で何が利用できるかはわかりませんが、一般的に行われていること、およびコード サンプル #1 に本当に必要なのは、書き込み時の単一のリリース バリアと読み取り時の単一の取得バリアです。
void A()
{
_answer = 123;
WriteWithReleaseBarrier(_complete, true); // "publish" values
}
void B()
{
if (ReadWithAcquire(_complete)) // subscribe
{
Console.WriteLine (_answer);
}
}
「サブスクライブ」という言葉は、状況を説明するためにあまり使用されませんが、「公開」は使用されます。スレッドに関する Herb Sutter の記事を読むことをお勧めします。
これにより、障壁が正確に適切な場所に配置されます。
コード サンプル #2 の場合、これは実際にはメモリ バリアの問題ではなく、コンパイラの最適化の問題complete
です。レジスタに保持されています。のように、メモリバリアはそれを強制的に外に出しますがvolatile
、おそらく外部関数を呼び出す場合もそうです-コンパイラがその外部関数が変更されたかどうかを判断できない場合、complete
メモリから再読み取りします。つまり、関数のアドレスを渡す可能性がありcomplete
ます (コンパイラが詳細を調べることができない場所で定義されています):
while (!complete)
{
some_external_function(&complete);
}
関数が を変更しないcomplete
場合でも、コンパイラが確信が持てない場合は、レジスタをリロードする必要があります。
つまり、コード 1 とコード 2 の違いは、コード 1 は A と B が別々のスレッドで実行されている場合にのみ問題があるということです。コード 2 は、シングル スレッド マシンでも問題が発生する可能性があります。
実際、もう 1 つの質問は、コンパイラは while ループを完全に削除できるかということです。他のコードでは到達できないと思われる場合complete
は、なぜですか? つまりcomplete
、レジスタに移動することを決定した場合、ループを完全に削除することもできます。
編集:opcからのコメントに答えるには(私の答えはコメントブロックには大きすぎます):
バリア 3 は、保留中の読み取り (および書き込み) 要求を CPU に強制的にフラッシュさせます。
したがって、_complete を読み取る前に他の読み取りがあった場合を想像してください。
void B {}
{
int x = a * b + c * d; // read a,b,c,d
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
...
バリアがなければ、CPU は次の 5 つの読み取り要求をすべて「保留中」にする可能性があります。
a,b,c,d,_complete
バリアがなければ、プロセッサはこれらの要求を並べ替えてメモリ アクセスを最適化できます (つまり、_complete と 'a' が同じキャッシュ ラインなどにある場合)。
バリアを使用すると、_complete が要求として投入される前に、CPU はメモリから a、b、c、d を取得します。確実に 'b' (たとえば) が _complete の前に読み取られるようにします。つまり、並べ替えはありません。
問題は、それによってどのような違いが生じるかということです。
a、b、c、d が _complete から独立していれば問題ありません。障壁がすることはすべて、物事を遅くすることです。そうそう、あとで_complete
読む。したがって、データはより新鮮です。読み取りの前に sleep(100) またはビジー待機 for ループをそこに置くと、それも「より新鮮」になります! :-)
ポイントは、相対的に保つことです。データは、他のデータと比較して BEFORE/AFTER で読み取り/書き込みする必要がありますか? それが問題です。
そして、記事の著者を侮辱しないために、彼は「もしBがAを追いかけたら...」と述べています。彼が A の後の B がコードにとって重要であると想像しているのか、to コードで観察できるのか、それとも単に取るに足らないものなのか、正確には明らかではありません。