2012-05-01 43 views
6

Trong luận thuyết tuyệt vời của ông về luồng trong C#, Joseph Albahari đề xuất chương trình đơn giản sau đây để chứng minh tại sao chúng ta cần sử dụng một số hình thức hàng rào bộ nhớ xung quanh dữ liệu được đọc và ghi chủ đề. Chương trình không bao giờ kết thúc nếu bạn biên dịch nó trong chế độ Release và tự do chạy nó mà không cần gỡ rối:biến chia sẻ giữa hai luồng hoạt động khác với thuộc tính được chia sẻ

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 
    } 

Câu hỏi của tôi là, tại sao phiên bản sửa đổi một chút trong những chương trình trên sau không còn chặn vô thời hạn ??

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!!! 
    } 
} 

Trong khi những điều sau đây vẫn khối vô thời hạn:

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!!! 
    } 
} 

Như nào sau đây:

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

Tiêu đề của câu hỏi của bạn rộng hơn cần phải đề cập đến tài liệu đang được đề cập đến. Không phải tất cả các mã đều đơn giản như thế này. –

+0

Bạn có so sánh IL của cả hai chương trình không? – Oded

+0

tôi đã so sánh IL nhưng không thực sự nhìn thấy bất cứ điều gì mà sẽ dẫn tôi vào một lời giải thích – dmg

Trả lời

7

Trong ví dụ đầu tiên Complete là biến thành viên và có thể được lưu trong bộ nhớ cache để đăng ký cho từng chuỗi. Vì bạn không sử dụng khóa, các cập nhật cho biến đó có thể không bị xóa sang bộ nhớ chính và luồng khác sẽ thấy giá trị cũ cho biến đó.

Trong ví dụ thứ hai, trong đó Complete là thuộc tính, bạn thực sự đang gọi một hàm trên đối tượng Foo để trả về một giá trị. Đoán của tôi sẽ là trong khi các biến đơn giản có thể được lưu trữ trong sổ đăng ký, trình biên dịch có thể không phải lúc nào cũng tối ưu hóa các thuộc tính thực tế theo cách đó.

EDIT:

Về việc tối ưu hóa các đặc tính tự động - Tôi không nghĩ rằng có điều gì đảm bảo bởi các đặc điểm kỹ thuật trong vấn đề đó. Bạn chủ yếu là ngân hàng về việc có hay không trình biên dịch/thời gian chạy sẽ có thể tối ưu hóa ra khỏi getter/setter hay không.

Trong trường hợp nó nằm trên cùng một đối tượng, có vẻ như nó. Trong trường hợp khác, nó có vẻ như nó không. Dù bằng cách nào, tôi sẽ không đặt cược vào nó. Cách dễ nhất để giải quyết vấn đề này là sử dụng biến thành viên đơn giản và đánh dấu là volotile để đảm bảo rằng nó luôn được đồng bộ hóa với bộ nhớ chính.

+0

Làm thế nào về ví dụ cuối cùng tôi vừa thêm vào? – dmg

+0

@dmg - đã chỉnh sửa câu trả lời của tôi. Kể từ khi spec không thực hiện bất kỳ đảm bảo về nó, nó đi xuống để đặt cược vào cách trình biên dịch có thể hoặc không thể tối ưu hóa các thuộc tính tự động. –

+0

có vẻ như đó là những gì đang xảy ra. nếu thuộc tính Complete thuộc về lớp này, thì nó được tối ưu hóa đi, nhưng nếu nó thuộc về một lớp khác, thì nó không phải là. – dmg

5

Điều này là do trong đoạn đầu tiên mà bạn cung cấp, bạn đã thực hiện một biểu thức lambda rằng đóng trên giá trị boolean complete - vì vậy, khi trình biên dịch viết lại nó, nó sẽ chụp một bản sao của giá trị, không phải là một tham chiếu. Tương tự như vậy, trong phần thứ hai, nó chụp một tham chiếu thay vì một bản sao, do đóng đối tượng Foo và do đó khi bạn thay đổi giá trị cơ bản, thay đổi được nhận thấy do tham chiếu.

+0

Bạn có thể giải thích cách 'hoàn thành' bị bắt bởi giá trị không? Tôi hy vọng nó sẽ bị bắt bởi tham chiếu, vì đây là những gì thường xảy ra trong một biểu thức lambda. –

+1

'bool' là một kiểu dữ liệu giá trị, vì vậy không thể chụp bằng tham chiếu. – Tejs

+0

Tôi vừa thêm một đoạn mã khác. Trình biên dịch có tối ưu hóa trường thành viên công khai Hoàn thành theo cách tương tự như biến bool cục bộ, nhưng không thể thực hiện tối ưu hóa tương tự nếu trường thành viên công khai được thay thế bằng thuộc tính? – dmg

3

Các câu trả lời khác giải thích những gì diễn ra theo thuật ngữ chính xác. Hãy để tôi xem nếu tôi có thể giải thích nó bằng tiếng Anh.

Ví dụ đầu tiên cho biết "Vòng lặp cho đến khi vị trí biến này là đúng". Chủ đề mới tạo một bản sao của vị trí biến đó (vì nó là một loại giá trị) và tiến hành lặp lại mãi mãi. Nếu biến đã xảy ra là một kiểu tham chiếu, nó sẽ tạo một bản sao của tham chiếu, nhưng vì tham chiếu đã xảy ra để trỏ tới cùng một vị trí bộ nhớ mà nó đã làm việc.

Ví dụ thứ hai cho biết "Vòng lặp cho đến khi phương thức này (bộ thu hồi) trả về giá trị đúng". Chủ đề mới không thể tạo một bản sao của một phương thức, do đó nó tạo một bản sao tham chiếu đến cá thể của lớp đang được đề cập đến, và liên tục gọi hàm getter trên cá thể đó cho đến khi nó trả về true (liên tục đọc cùng một vị trí biến được đặt đúng trong chủ đề chính).

Ví dụ thứ ba giống như ví dụ thứ nhất. Thực tế là biến đóng đã xảy ra là một thành viên của một cá thể lớp khác không liên quan.

+0

Vì vậy, tôi đoán trong trình biên dịch ví dụ thứ tư tối ưu hóa cuộc gọi đến tài sản có được tĩnh và xử lý nó như thể nó là một bản sao của một biến? – dmg

+0

Trong ví dụ thứ tư (xin lỗi, đã không nhìn thấy nó cho đến bây giờ) Tôi không chắc chắn những gì đang xảy ra. Nghi ngờ của tôi sẽ là một cái gì đó giống như nó được inlining getter, dẫn đến một bản sao của biến, nhưng tôi không chắc chắn. Tôi đã mong đợi rằng để không chặn. –

0

Để mở rộng trên Eric Petroelje's answer.

Nếu chúng ta viết lại chương trình như sau (hành vi giống hệt nhau, nhưng tránh hàm lambda giúp dễ dàng đọc được giải mã), chúng ta có thể giải thích nó và xem nó thực sự có ý nghĩa gì để "lưu trữ giá trị của trường một đăng ký"

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(); 
    } 
} 

Chúng tôi nhận được các hành vi sau đây:

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

trong x86 phát hành, JIT CLR biên dịch trong khi (foo.Complete!) vào mã này:

Hoàn thành là một trường:

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 

Hai dòng cuối cùng là vấn đề. Nếu eax bằng không, thì nó sẽ chỉ ngồi đó trong một vòng lặp vô hạn nói "là EAX không?", mà không có bất kỳ mã nào thay đổi giá trị của eax!

Complete là một tài sản:

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 

này thực sự trông như mã đẹp hơn. Trong khi JIT đã phác thảo thuộc tính getter (nếu không bạn sẽ thấy một số hướng dẫn call chuyển sang các hàm khác) vào một số mã đơn giản để đọc trực tiếp trường Complete, vì nó không được phép lưu biến, khi nó tạo vòng lặp, liên tục đọc bộ nhớ hơn và hơn nữa, thay vì chỉ pointlessly đọc thanh ghi

trong x64-phát hành, 64 bit CLR JIT biên dịch trong khi (! foo.Complete) vào mã này

Complete là một lĩnh vực :

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 

Hoàn thành là tài sản

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 

Phiên bản 64-bit JIT đang làm điều tương tự cho cả tài sản và các lĩnh vực, trừ khi đó là một lĩnh vực đó là "trải ra" phiên đầu tiên của vòng lặp - điều này về cơ bản đặt dấu if(foo.Complete) { jump past the loop code } ở phía trước của nó đối với một số lý do.

Trong cả hai trường hợp, nó đang làm một điều tương tự như JIT x86 khi giao dịch với một tài sản:
- Nó inlines phương pháp để một bộ nhớ trực tiếp đọc - Nó không cache nó, và lại đọc giá trị mỗi lần

Tôi không chắc liệu CLR 64 bit không được phép lưu trữ giá trị trường trong thanh ghi như đăng ký 32 bit, nhưng nếu có, nó không làm phiền. Có lẽ nó sẽ trong tương lai?

Ở mức độ nào, điều này minh họa cách hoạt động của nền tảng phụ thuộc và có thể thay đổi. Tôi hy vọng điều này sẽ giúp :-)

Các vấn đề liên quan