Mã này bị treo ở chế độ phát hành nhưng hoạt động tốt ở chế độ gỡ lỗi


110

Tôi đã xem qua điều này và muốn biết lý do của hành vi này trong chế độ gỡ lỗi và phát hành.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
chính xác thì sự khác biệt trong hành vi là gì?
Mong Zhu

4
Nếu đó là java, tôi sẽ giả định rằng trình biên dịch không thấy các bản cập nhật cho biến 'biên dịch'. Việc thêm 'variable' vào khai báo biến sẽ khắc phục điều đó (và biến nó thành trường tĩnh).
Sebastian


4
Lưu ý: đây là lý do tại sao phát triển đa luồng có những thứ như mutexes và các phép toán nguyên tử. Khi bạn bắt đầu sử dụng đa luồng, bạn cần phải xem xét một loạt các vấn đề bộ nhớ bổ sung đáng chú ý mà trước đây không rõ ràng. Các công cụ đồng bộ hóa luồng như mutexes sẽ giải quyết được điều này.
Cort Ammon

5
@DavidSchwartz: Chắc chắn điều đó được phép . Và cho phép trình biên dịch, thời gian chạy và CPU làm cho kết quả của mã đó khác với những gì bạn mong đợi. Đặc biệt, C # không được phép thực hiện truy cập bool phi giải phẫu , nhưng nó được phép di chuyển ngược thời gian đọc không bay hơi. Ngược lại, Double không có hạn chế như vậy về tính nguyên tử; Việc đọc và ghi kép trên hai luồng khác nhau mà không đồng bộ hóa được phép bị xé.
Eric Lippert

Câu trả lời:


149

Tôi đoán rằng trình tối ưu hóa bị đánh lừa bởi thiếu từ khóa 'dễ bay hơi' trên isCompletebiến.

Tất nhiên, bạn không thể thêm nó, vì nó là một biến cục bộ. Và tất nhiên, vì nó là một biến cục bộ, nó không cần thiết chút nào, bởi vì các biến địa phương được lưu giữ trên chồng và chúng đương nhiên luôn "tươi".

Tuy nhiên , sau khi biên dịch, nó không còn là một biến cục bộ nữa. Vì nó được truy cập trong một đại biểu ẩn danh, mã được tách và nó được dịch thành một lớp trợ giúp và trường thành viên, giống như:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

Bây giờ tôi có thể tưởng tượng rằng trình biên dịch / tối ưu hóa JIT trong môi trường đa luồng, khi xử lý TheHelperlớp, thực sự có thể lưu giá trị falsevào bộ nhớ cache trong một số thanh ghi hoặc khung ngăn xếp ở đầu Body()phương thức và không bao giờ làm mới nó cho đến khi phương thức kết thúc. Đó là bởi vì KHÔNG CÓ ĐẢM BẢO rằng luồng & phương thức sẽ KHÔNG kết thúc trước khi "= true" được thực thi, vì vậy nếu không có gì đảm bảo, thì tại sao không lưu vào bộ nhớ cache và tăng hiệu suất đọc đối tượng heap một lần thay vì đọc nó mọi lúc sự lặp lại.

Đây chính là lý do tại sao từ khóa volatiletồn tại.

Để lớp trợ giúp này chính xác tốt hơn một chút 1) trong môi trường đa luồng, nó phải có:

    public volatile bool isComplete = false;

nhưng, tất nhiên, vì nó là mã được tạo tự động, bạn không thể thêm nó. Một cách tiếp cận tốt hơn sẽ là thêm một số lock()s xung quanh việc đọc và ghi isCompleted, hoặc sử dụng một số tiện ích đồng bộ hóa / phân luồng / tác vụ sẵn sàng sử dụng khác thay vì cố gắng làm điều đó bằng kim loại trần (mà nó sẽ không phải là kim loại trần, vì nó là C # trên CLR với GC, JIT và (..)).

Sự khác biệt trong chế độ gỡ lỗi xảy ra có thể là do trong chế độ gỡ lỗi, nhiều tính năng tối ưu bị loại trừ, vì vậy bạn có thể gỡ lỗi mã mà bạn thấy trên màn hình. Do đó while (!isComplete)không được tối ưu hóa để bạn có thể đặt một điểm ngắt ở đó và do đó isCompletekhông được lưu trữ tích cực trong một thanh ghi hoặc ngăn xếp khi bắt đầu phương thức và được đọc từ đối tượng trên heap ở mỗi lần lặp vòng lặp.

BTW. Đó chỉ là suy đoán của tôi về điều đó. Tôi thậm chí đã không cố gắng biên dịch nó.

BTW. Nó dường như không phải là một lỗi; nó giống như một tác dụng phụ rất khó hiểu. Ngoài ra, nếu tôi nói đúng về nó, thì đó có thể là một sự thiếu sót về ngôn ngữ - C # nên cho phép đặt từ khóa 'variable' trên các biến cục bộ được nắm bắt và thăng cấp cho các trường thành viên trong phần đóng.

1) xem bên dưới để biết nhận xét từ Eric Lippert về volatilevà / hoặc bài viết rất thú vị này cho thấy mức độ phức tạp liên quan đến việc đảm bảo rằng mã dựa vào volatilean toàn ..uh, tốt ..uh, hãy nói OK.


2
@EricLippert: chà, cảm ơn bạn rất nhiều vì đã xác nhận điều đó quá nhanh! Bạn nghĩ thế nào, liệu có cơ hội nào mà trong một phiên bản nào đó trong tương lai, chúng ta có thể nhận được volatiletùy chọn về các biến cục bộ nắm bắt để đóng không? Tôi tưởng tượng nó có thể là một chút khó khăn để xử lý bởi trình biên dịch ..
Quetzalcoatl

7
@quetzalcoatl: Tôi sẽ không tính đến việc tính năng đó sẽ sớm được thêm vào. Đây là kiểu mã hóa mà bạn muốn làm nản lòng và không dễ dàng hơn . Và bên cạnh đó, làm cho mọi thứ dễ bay hơi không nhất thiết giải quyết được mọi vấn đề. Đây là một ví dụ mà mọi thứ đều không ổn định và chương trình vẫn sai; bạn có thể tìm thấy lỗi? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Hiểu. Tôi đã từ bỏ việc cố gắng hiểu tối ưu hóa đa luồng ... thật điên rồ vì nó phức tạp như thế nào.
InBetween

10
@Pikoh: Một lần nữa, hãy suy nghĩ như một người tối ưu hóa. Bạn có một biến được tăng dần nhưng không bao giờ được đọc. Một biến không bao giờ được đọc có thể bị xóa hoàn toàn.
Eric Lippert

4
@EricLippert bây giờ tâm trí tôi đã nhấp chuột. Chủ đề này đã được rất nhiều thông tin, cảm ơn bạn rất nhiều, thực sự.
Pikoh

82

Câu trả lời của quetzalcoatl là đúng. Để làm sáng tỏ hơn về nó:

Trình biên dịch C # và CLR jitter được phép thực hiện nhiều tối ưu hóa tuyệt vời giả định rằng luồng hiện tại là luồng duy nhất đang chạy. Nếu những tối ưu hóa đó làm cho chương trình không chính xác trong một thế giới mà luồng hiện tại không phải là luồng duy nhất đang chạy thì đó là vấn đề của bạn . Bạn được yêu cầu viết các chương trình đa luồng cho trình biên dịch và jitter biết bạn đang làm những thứ đa luồng điên rồ nào.

Trong trường hợp cụ thể này, jitter được phép - nhưng không bắt buộc - quan sát rằng biến không thay đổi bởi thân vòng lặp và do đó kết luận rằng - vì theo giả định đây là luồng duy nhất đang chạy - biến sẽ không bao giờ thay đổi. Nếu nó không bao giờ thay đổi thì biến cần được kiểm tra sự thật một lần , không phải mỗi lần qua vòng lặp. Và đây là thực tế những gì đang xảy ra.

Làm thế nào để giải quyết điều này? Không viết các chương trình đa luồng . Đa luồng rất khó để thực hiện đúng, ngay cả đối với các chuyên gia. Nếu bạn phải, thì hãy sử dụng các cơ chế cấp cao nhất để đạt được mục tiêu của bạn . Giải pháp ở đây là không làm cho biến số dễ bay hơi. Giải pháp ở đây là viết một tác vụ có thể hủy và sử dụng cơ chế hủy bỏ Thư viện song song Tác vụ . Hãy để TPL lo lắng về việc nhận đúng logic luồng và việc hủy gửi đúng cách giữa các luồng.


1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Madara's Ghost

14

Tôi đã gắn liền với quá trình chạy và nhận thấy (nếu tôi không mắc lỗi, tôi không thực hành nhiều với điều này) rằng Threadphương pháp được dịch thành như sau:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(chứa giá trị của isComplete) được tải lần đầu tiên và không bao giờ được làm mới.


8

Không thực sự là một câu trả lời, nhưng để làm sáng tỏ thêm một số vấn đề:

Vấn đề dường như là khi nào iđược khai báo bên trong thân lambda chỉ được đọc trong biểu thức gán. Nếu không, mã hoạt động tốt ở chế độ phát hành:

  1. i được khai báo bên ngoài thân lambda:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i không được đọc trong biểu thức gán:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i cũng được đọc ở một nơi khác:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Cá cược của tôi là một số trình biên dịch hoặc tối ưu hóa JIT liên quan đến việc ilàm rối tung mọi thứ. Ai đó thông minh hơn tôi có thể sẽ có thể làm sáng tỏ vấn đề này.

Tuy nhiên, tôi sẽ không lo lắng quá nhiều về điều đó, bởi vì tôi không biết mã tương tự sẽ thực sự phục vụ cho mục đích nào.


1
xem câu trả lời của tôi, tôi khá chắc chắn rằng nó là tất cả về 'bay hơi' từ khóa thatcannot được thêm vào một biến địa phương (mà thực sự được thăng chức sau một trường thành viên trong việc đóng cửa) ..
Quetzalcoatl
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.