2

次のコード サンプル ( http://www.albahari.com/threading/part4.aspx#_NonBlockingSynchから取得)について質問があります。

class Foo
{
   int _answer;
   bool _complete;

   void A()
   {
       _answer = 123;
       Thread.MemoryBarrier();    // Barrier 1
       _complete = true;
       Thread.MemoryBarrier();    // Barrier 2
   }

    void B()
    {
       Thread.MemoryBarrier();    // Barrier 3
       if (_complete)
       {  
          Thread.MemoryBarrier(); // Barrier 4
          Console.WriteLine (_answer);
       }
    }
 }

これに続いて、次の説明が続きます。

「バリア 1 と 4 は、この例が「0」を書き込むのを防ぎます。バリア 2 と 3 は、鮮度を保証します。B が A の後に実行された場合、_complete の読み取りが true と評価されることを保証します。」

メモリバリアの使用が命令の並べ替えにどのように影響するかは理解していますが、言及されているこの「鮮度保証」とは何ですか?

この記事の後半では、次の例も使用されます。

static void Main()
{
    bool complete = false; 
    var t = new Thread (() =>
    {
        bool toggle = false;
        while (!complete) 
        {
           toggle = !toggle;
           // adding a call to Thread.MemoryBarrier() here fixes the problem
        }

    });

    t.Start();
    Thread.Sleep (1000);
    complete = true;
    t.Join();  // Blocks indefinitely
}

この例の後に、次の説明が続きます。

「完全な変数が CPU レジスタにキャッシュされているため、このプログラムは決して終了しません。while ループ内に Thread.MemoryBarrier への呼び出しを挿入する (または読み取り完了をロックする) と、エラーが修正されます。」

もう一度…ここで何が起こりますか?

4

4 に答える 4

6

最初のケースでは、バリア 1 が確実_answerに BEFORE に書き込まれ_completeます。コードの記述方法や、コンパイラまたは CLR が CPU に指示する方法に関係なく、メモリ バスの読み取り/書き込みキューは要求を並べ替えることができます。バリアは基本的に「続行する前にキューをフラッシュする」と言います。同様に、Barrier 4 は_answerAFTER が読み取られることを確認します_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 はまだ_completetrue と見なさない可能性があります。つまり、「新鮮」ではありません。

しかし、おそらくマシンを開いて を見ることはできません_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 コードで観察できるのか、それとも単に取るに足らないものなのか、正確には明らかではありません。

于 2009-11-15T06:12:21.607 に答える
1

コードサンプル #1:

各プロセッサ コアには、メモリの一部のコピーを含むキャッシュが含まれています。キャッシュが更新されるまで、少し時間がかかる場合があります。メモリ バリアは、キャッシュがメイン メモリと同期されることを保証します。たとえば、ここにバリア 2 と 3 がなかった場合は、次の状況を考えてみてください。

プロセッサ 1 は A() を実行します。_complete の新しい値をキャッシュに書き込みます (ただし、まだメイン メモリに書き込む必要はありません)。

プロセッサー 2 は B() を実行します。_complete の値を読み取ります。この値が以前にキャッシュにあった場合、最新ではない (つまり、メイン メモリと同期されていない) 可能性があるため、更新された値を取得できません。

コードサンプル #2:

通常、変数はメモリに格納されます。ただし、1 つの関数で値が複数回読み取られるとします。最適化として、コンパイラは値を CPU レジスタに 1 回読み取ってから、必要になるたびにレジスタにアクセスすることを決定する場合があります。これははるかに高速ですが、関数が別のスレッドから変数への変更を検出するのを防ぎます。

ここでのメモリ バリアは、関数に変数値をメモリから再読み込みさせます。

于 2009-11-14T20:20:02.210 に答える
0

「鮮度」の保証とは、バリア 2 と 3 が、値が_completeたまたまメモリに書き込まれるたびにではなく、できるだけ早く表示されるように強制することを意味します。

answerバリア 1 と 4 により、 read 後に read が確実に読み込まれるため、一貫性の観点からは実際には不要completeです。

于 2009-11-16T00:09:04.610 に答える
0

Thread.MemoryBarrier() を呼び出すと、変数の実際の値でレジスタ キャッシュがすぐに更新されます。

_complete最初の例では、メソッドを設定した直後と使用する直前にメソッドを呼び出すことによって、「鮮度」が提供されます。2 番目の例では、変数の初期falsecompleteはスレッド自身の領域にキャッシュされ、実行中のスレッドの「内側」から実際の「外側」の値をすぐに確認するために再同期する必要があります。

于 2009-11-14T20:15:29.457 に答える