6

Joseph Albahari は、C# のスレッド化に関する優れた論文の中で、次の簡単なプログラムを提案して、複数のスレッドによって読み書きされるデータの周りになんらかの形式のメモリ フェンシングを使用する必要がある理由を示しました。リリース モードでコンパイルし、デバッガーなしでフリーランしても、プログラムは決して終了しません。

  static void Main()
  {
     bool complete = false;
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     complete = true;                  
     t.Join(); // Blocks indefinitely
  }

私の質問は、上記のプログラムの次のわずかに変更されたバージョンが無期限にブロックされなくなったのはなぜですか??

class Foo
{
  public bool Complete { get; set; }
}

class Program
{
  static void Main()
  {
     var foo = new Foo();
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!foo.Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     foo.Complete = true;                  
     t.Join(); // No longer blocks indefinitely!!!
  }
}

一方、以下は依然として無期限にブロックされます。

class Foo
{
  public bool Complete;// { get; set; }
}

class Program
{
  static void Main()
  {
     var foo = new Foo();
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!foo.Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     foo.Complete = true;                  
     t.Join(); // Still blocks indefinitely!!!
  }
}

以下も同様です。

class Program
{
  static bool Complete { get; set; }

  static void Main()
  {
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     Complete = true;                  
     t.Join(); // Still blocks indefinitely!!!
  }
}
4

4 に答える 4

7

最初の例Completeはメンバー変数であり、スレッドごとにレジスターにキャッシュできます。ロックを使用していないため、その変数への更新がメイン メモリにフラッシュされない可能性があり、他のスレッドはその変数の古い値を参照します。

2 番目の例でCompleteは、 はプロパティであり、実際には Foo オブジェクトで関数を呼び出して値を返しています。私の推測では、単純な変数はレジスターにキャッシュされている可能性がありますが、コンパイラーは実際のプロパティを常にそのように最適化するとは限りません。

編集:

自動プロパティの最適化について - 仕様で保証されているものはないと思います。基本的に、コンパイラ/ランタイムがゲッター/セッターを最適化できるかどうかに依存しています。

同じオブジェクト上にある場合は、そのように見えます。それ以外の場合は、そうではないようです。いずれにせよ、私はそれに賭けません。これを解決する最も簡単な方法は、単純なメンバー変数を使用することであり、マークvolotileは常にメイン メモリと同期されるようにすることです。

于 2012-05-01T15:20:33.873 に答える
5

これは、提供した最初のスニペットで、ブール値を閉じるラムダ式を作成したためですcomplete。したがって、コンパイラがそれを書き換えると、参照ではなく値のコピーがキャプチャされます。同様に、2番目のものでは、Fooオブジェクトを閉じるため、コピーではなく参照をキャプチャしているため、基になる値を変更すると、参照のために変更が通知されます。

于 2012-05-01T15:12:23.207 に答える
3

他の回答は、技術的に正しい用語で何が起こるかを説明しています。英語で説明できるかどうか見てみましょう。

最初の例は、「この変数の場所が true になるまでループする」というものです。新しいスレッドは、その変数の場所のコピーを作成し (値型であるため)、永遠にループを続けます。変数がたまたま参照型であった場合、参照のコピーが作成されますが、参照がたまたま同じメモリ位置を指していたため、機能していたはずです。

2 番目の例は、「このメソッド (ゲッター) が true を返すまでループする」ことを示しています。新しいスレッドはメソッドのコピーを作成できないため、問題のクラスのインスタンスへの参照のコピーを作成し、true が返されるまでそのインスタンスの getter を繰り返し呼び出します (設定されている同じ変数の場所を繰り返し読み取ります)。メインスレッドで true にする)。

3 番目の例は、最初の例と同じです。閉じた変数がたまたま別のクラス インスタンスのメンバーであるという事実は関係ありません。

于 2012-05-01T15:26:16.223 に答える
0

EricPetroeljeの答えを拡張します。

プログラムを次のように書き直すと(動作は同じですが、ラムダ関数を回避すると分解が読みやすくなります)、プログラムを分解して、「フィールドの値をレジスタにキャッシュする」ことの実際の意味を確認できます。

class Foo
{
    public bool Complete; // { get; set; }
}

class Program
{
    static Foo foo = new Foo();

    static void ThreadProc()
    {
        bool toggle = false;
        while (!foo.Complete) toggle = !toggle;

        Console.WriteLine("Thread done");
    }

    static void Main()
    {
        var t = new Thread(ThreadProc);
        t.Start();
        Thread.Sleep(1000);
        foo.Complete = true;
        t.Join();
    }
}

次の動作が得られます。

                Foo.Complete is a Field  |   Foo.Complete is a Property
x86-RELEASE  |      loops forever        |          completes  
x64-RELEASE  |        completes          |          completes  

x86リリースでは、CLR JITはwhile(!foo.Complete)を次のコードにコンパイルします。

Completeはフィールドです:

004f0153 a1f01f2f03      mov     eax,dword ptr ds:[032F1FF0h] # Put a pointer to the Foo object in EAX
004f0158 0fb64004        movzx   eax,byte ptr [eax+4]  # Put the value pointed to by  [EAX+4] into EAX (this basically puts the value of .Complete into EAX)
004f015c 85c0            test    eax,eax   # Is EAX zero? (is .Complete false?)
004f015e 7504            jne     004f0164  # If it is not, exit the loop
# start of loop
004f0160 85c0            test    eax,eax   # Is EAX zero? (is .Complete false?)
004f0162 74fc            je      004f0160  # If it is, goto start of loop

最後の2行が問題です。eaxがゼロの場合、eaxの値を変更するコードがなくても、「EAXはゼロですか?」という無限ループになります。

Completeはプロパティです:

00220155 a1f01f3a03      mov     eax,dword ptr ds:[033A1FF0h] # Put a pointer to the Foo object in EAX
0022015a 80780400        cmp     byte ptr [eax+4],0 # Compare the value at [EAX+4] with zero (is .Complete false?)
0022015e 74f5            je      00220155 # If it is, goto 2 lines up

これは実際にはより良いコードのように見えます。JITはプロパティゲッターをインライン化して(そうでない場合は、他の関数にいくつかの命令が送信されるのを確認します)、フィールドを直接call読み取るためのいくつかの単純なコードにインライン化しますが、変数をキャッシュすることは許可されていないため、ループを生成するときに繰り返し読み取りますCompleteレジスタを無意味に読み取るのではなく、メモリを何度も繰り返します

x64リリースでは、64ビットCLR JITはwhile(!foo.Complete)をこのコードにコンパイルします

Completeはフィールドです:

00140245 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table
0014024f 488b00          mov     rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX
00140252 0fb64808        movzx   ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX
00140256 85c9            test    ecx,ecx # Is ECX zero ? (is the .Complete field false?)
00140258 751b            jne     00140275 # If nonzero/true, exit the loop
0014025a 660f1f440000    nop     word ptr [rax+rax]  # Do nothing!
# start of loop
00140260 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 
0014026a 488b00          mov     rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 
0014026d 0fb64808        movzx   ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX
00140271 85c9            test    ecx,ecx # Is ECX Zero ? (is the .Complete field true?)
00140273 74eb            je      00140260 # If zero/false, go to start of loop

コンプリートはプロパティです

00140250 48b8d82fe11200000000 mov rax,12E12FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table
0014025a 488b00          mov     rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX
0014025d 0fb64008        movzx   eax,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in EAX
00140261 85c0            test    eax,eax # Is EAX 0 ? (is the .Complete field false?)
00140263 74eb            je      00140250 # If zero/false, go to the start

64ビットJITは、プロパティとフィールドの両方で同じことを実行しますが、フィールドの場合、ループの最初の反復で「展開」されます。これにより、基本的にif(foo.Complete) { jump past the loop code }何らかの理由でその前にが配置されます。

どちらの場合も、プロパティを処理するときにx86 JITと同様のことを行います
。-メソッドをダイレクトメモリ読み取りにインライン化します-キャッシュせず、毎回値を再読み取りします

64ビットCLRが32ビットのCLRのようにフィールド値をレジスタにキャッシュすることを許可されているかどうかはわかりませんが、キャッシュできる場合は、そうする必要はありません。おそらくそれは将来的になりますか?

いずれにせよ、これは、動作がプラットフォームに依存し、変更される可能性があることを示しています。お役に立てば幸いです:-)

于 2012-08-02T23:48:31.890 に答える