2012-05-01 91 views
6

行爲不同在他的優秀論文在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!!! 
    } 
} 
+0

你的問題的標題是更廣泛的比它需要的是覆蓋問題的材料。並非所有代碼都如此簡單。 –

+0

你比較過這兩個程序的IL嗎? – Oded

+0

我確實比較了IL,但沒有看到任何可以幫我解釋的東西 – dmg

回答

7

在第一個例子是Complete成員變量,並可能在寄存器被高速緩存爲每個線程。由於您沒有使用鎖定,因此對該變量的更新可能不會刷新到主內存,另一個線程將看到該變量的陳舊值。

在第二個例子,其中Complete是一個屬性,你實際上是調用Foo對象上的函數返回一個值。我的猜測是,儘管簡單變量可能會緩存在寄存器中,但編譯器可能並不總是以這種方式優化實際屬性。

編輯:

關於自動性能優化 - 我不認爲這有什麼用在這方面的規範保證。你基本上是想知道編譯器/運行時是否能夠優化getter/setter。

在它是在同一個對象上的情況下,它看起來像是這樣。在另一種情況下,它似乎沒有。無論如何,我不會賭它。解決這個問題的最簡單方法是使用一個簡單的成員變量,標記爲volotile以確保它總是與主內存同步。

+0

我剛剛添加的最後一個例子呢? – dmg

+0

@dmg - 編輯我的答案。由於規範沒有對此做任何保證,因此我們下注賭注編譯器如何優化自動屬性。 –

+0

似乎是這樣。如果Complete屬性屬於這個類,那麼它會被優化掉,但是如果它屬於不同的類,那麼它不是。 – dmg

5

這是因爲您提供的第一個片段,你犯了一個lambda表達式是關閉通過布爾值complete - 因此,當編譯器重寫它時,它會捕獲值的副本,而不是引用。同樣,在第二個中,由於關閉了Foo對象,因此它將捕獲參考而不是副本,因此,當您更改基礎值時,由於引用而引起更改。

+0

你能解釋'complete'是如何被價值捕獲的嗎?我希望它能被引用捕獲,因爲這是通常在lambda表達式中發生的事情。 –

+1

'bool'是一個值數據類型,所以不可能通過引用來捕獲。 – Tejs

+0

我剛剛添加了另一個代碼片段。編譯器是否以與本地bool變量相同的方式優化公共成員字段Complete,但是如果公共成員字段被屬性替換,則不能執行相同的優化? – dmg

3

其他答案解釋了在技術上正確的術語會發生什麼。讓我看看我能否用英文解釋它。

第一個例子說「循環,直到這個變量的位置爲真。」新線程創建該變量位置的副本(因爲它是一個值類型)並繼續循環。如果這個變量碰巧是一個引用類型,它就會創建一個引用的副本,但是由於引用碰巧指向了相同的內存位置,它將會起作用。

第二個例子說「循環直到這個方法(getter)返回true」。新線程不能創建一個方法的副本,所以它創建了一個引用到所討論的類的實例的副本,並重復調用該實例的getter直到它返回true(重複讀取設置的相同變量位置在主線程中爲true)。

第三個例子是一樣的第一個。事實上,閉合變量恰好是另一個類實例的成員是不相關的。

+0

所以我想在第四個例子中,編譯器優化掉了對靜態get屬性的調用,並將它看作是一個變量的副本? – dmg

+0

在第四個示例中(對不起,直到現在還沒有看到)我不確定發生了什麼。我的懷疑會像內聯吸氣劑一樣,導致變量的副本,但我不確定。我本來期望不會阻止。 –

0

要擴大Eric Petroelje's answer

如果我們按照以下方式重寫程序(行爲是相同的,但是避免使用lambda函數可以更容易地讀取反彙編),我們可以將其解析並查看它實際上意味着什麼「緩存字段的值一登記」

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編譯時(foo.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 

的最後兩行都是問題。如果eax爲零,那麼它只會坐在一個無限循環中說「EAX零?」,沒有任何代碼可以改變eax的值!

完全是一個屬性:

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內聯了屬性的getter(否則你會看到一些call說明會切換到其他功能)插入一些簡單的代碼來直接讀取Complete領域,因爲它不允許緩存變量,當它產生一個循環中,重複一遍又一遍讀取內存,而不僅僅是毫無意義的讀取寄存器

在x64的版本

,64位CLR JIT編譯,而(!foo.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位人做的寄存器緩存字段值,但如果是,它沒有打擾這樣做。也許將來呢?

無論如何,這說明行爲是如何依賴於平臺,並隨時可能更改。我希望這有助於:-)