2010-08-31 29 views
10

Tôi đã viết một chương trình Windows bằng C++ mà đôi khi sử dụng hai luồng: một luồng nền để thực hiện công việc tốn thời gian; và một chủ đề khác để quản lý giao diện đồ họa. Bằng cách này, chương trình vẫn đáp ứng với người dùng, điều này là cần thiết để có thể hủy bỏ một hoạt động nhất định. Các chủ đề giao tiếp thông qua biến số bool được chia sẻ, được đặt thành true khi chuỗi GUI báo hiệu chuỗi công nhân bị hủy. Đây là mã mà thực hiện hành vi này (tôi đã bị tước đi phần không liên quan):Có phải 'biến động' cần thiết trong mã C++ đa luồng này không?

mã thực thi bởi các thread GUI


class ProgressBarDialog : protected Dialog { 

    /** 
    * This points to the variable which the worker thread reads to check if it 
    * should abort or not. 
    */ 
    bool volatile* threadParameterAbort_; 

    ... 

    BOOL CALLBACK ProgressBarDialog::DialogProc(HWND dialog, UINT message, 
     WPARAM wParam, LPARAM lParam) { 

     switch(message) { 
      case WM_COMMAND : 
       switch (LOWORD(wParam)) { 

        ... 

        case IDCANCEL : 
        case IDC_BUTTON_CANCEL : 
         switch (progressMode_) { 
          if (confirmAbort()) { 
           // This causes the worker thread to be aborted 
           *threadParameterAbort_ = true; 
          } 
          break; 
         } 

         return TRUE; 
       } 
     } 

     return FALSE; 
    } 

    ... 

}; 

MÃ thực hiện bởi CÔNG NHÂN THREAD


class CsvFileHandler { 

    /** 
    * This points to the variable which is set by the GUI thread when this 
    * thread should abort its execution. 
    */ 
    bool volatile* threadParamAbort_; 

    ... 

    ParseResult parseFile(ItemList* list) { 
     ParseResult result; 

     ... 

     while (readLine(&line)) { 
      if ((threadParamAbort_ != NULL) && *threadParamAbort_) { 
       break; 
      } 

      ... 
     } 

     return result; 
    } 

    ... 

}; 

threadParameterAbort_ trong cả hai luồng đều trỏ đến biến số bool được khai báo trong cấu trúc được chuyển đến chuỗi công nhân khi tạo. Nó được khai báo là

bool volatile abortExecution_;

Câu hỏi của tôi là: sao tôi cần phải sử dụng volatile ở đây, và là mã trên đủ để đảm bảo rằng chương trình là thread-an toàn không? Con đường tôi đã lập luận để biện minh cho việc sử dụng các volatile đây (xem this question cho nền) là nó sẽ:

  • ngăn chặn việc đọc *threadParameterAbort_ sử dụng bộ nhớ cache và thay vào đó lấy giá trị từ bộ nhớ, và

  • ngăn trình biên dịch xóa mệnh đề if trong chuỗi công việc do tối ưu hóa.

(Đoạn sau đây là chỉ quan tâm đến các chủ đề an toàn của chương trình như vậy và không, tôi lặp lại, không liên quan đến tuyên bố rằng volatile dưới mọi hình thức cung cấp bất kỳ phương tiện đảm bảo chủ đề -Safety như tôi có thể nói, nó nên được thread-an toàn như thiết lập của một biến bool nên trong hầu hết, nếu không phải tất cả, kiến ​​trúc là một hoạt động nguyên tử. Nhưng tôi có thể sai. Và tôi cũng lo lắng về việc trình biên dịch có thể sắp xếp lại các hướng dẫn như phá vỡ an toàn luồng không. Nhưng tốt hơn là an toàn (không có ý định chơi chữ) hơn là xin lỗi.

CHỈNH SỬA: Một lỗi nhỏ trong từ ngữ của tôi khiến câu hỏi xuất hiện như thể tôi hỏi nếu volatile đủ để đảm bảo an toàn cho luồng. Đây không phải là ý định của tôi - volatile thực sự không đảm bảo an toàn luồng theo bất kỳ cách nào - nhưng điều tôi muốn hỏi là nếu mã được cung cấp ở trên thể hiện hành vi đúng để đảm bảo chương trình an toàn chỉ.

+1

Ghẻ tốt hơn [một nửa số đó] (http://stackoverflow.com/search?q=c%2B%2B+thread+volatile). IIRC gần đây nhất là [câu hỏi] (http://stackoverflow.com/questions/3372703/volatile-and-multithreading). – Dummy00001

+0

Câu hỏi ngày hôm qua từ cùng một người bao gồm cùng một nền tảng: http://stackoverflow.com/questions/3604569/what-kinds-of-optimizations-does-volatile-prevent-in-c. Nếu bạn không viết một trình điều khiển thiết bị và bạn không quen thuộc với trình biên dịch, bạn không cần 'biến động'. – Potatoswatter

+0

@Potatoswatter: Nhưng có vẻ như 'biến động' có thể được sử dụng khi viết mã đa luồng, như được hiển thị trong bài viết này (http://www.drdobbs.com/cpp/184403766) được liên kết trong một trong các câu trả lời. – gablin

Trả lời

14

Bạn không nên phụ thuộc vào biến động để đảm bảo an toàn luồng, vì mặc dù trình biên dịch sẽ đảm bảo rằng biến luôn được đọc từ bộ nhớ (và không phải là bộ đệm đăng ký), trong môi trường đa bộ xử lý. cũng được yêu cầu.

Thay vì sử dụng khóa chính xác xung quanh bộ nhớ dùng chung. Ổ khóa như một phần quan trọng thường rất nhẹ và trong trường hợp không có tranh chấp có thể sẽ được tất cả người dùng thực hiện. Chúng cũng sẽ chứa các rào cản bộ nhớ cần thiết.

Biến động chỉ nên được sử dụng cho bộ nhớ ánh xạ IO trong đó nhiều lần đọc có thể trả về các giá trị khác nhau. Tương tự như vậy đối với bộ nhớ ánh xạ ghi.

+0

Nhưng nếu tôi loại bỏ 'biến động', nó có thể xảy ra khi trình biên dịch nhầm lẫn tối ưu hóa mệnh đề' if' không? Hoặc có lẽ giá trị '* threadParamAbort_' luôn sử dụng bộ nhớ cache? Áp dụng một phần quan trọng thông qua một mutex hoặc semaphore đảm bảo rằng điều này không xảy ra? – gablin

+2

Một mutex hoặc semaphore là một hàm không thuần khiết. Nó có thể có các tác dụng phụ (thay đổi các biến toàn cầu) và do đó trình biên dịch có thể không giả định giá trị của * threadParamAbort_ sẽ không thay đổi. Và sẽ phải được đọc lại sau khi khóa hoặc tín hiệu khóa hoặc tín hiệu quan trọng. Và vì chỉ có 1 luồng có thể giữ một phần quan trọng tại một thời điểm, không có gì sai với việc sắp xếp lại các lệnh bên trong khóa. Chỉ cần đảm bảo bạn áp dụng khóa chính xác. – doron

+0

Được rồi, do đó, bằng cách gói mệnh đề 'if' với một phần quan trọng sẽ ngăn trình biên dịch tối ưu hóa nó đi, đúng không? Mặc dù 'threadParameterAbort_' trong thực tế không phải là một biến toàn cầu? Nếu vậy, thì không cần đến 'volatile' vì điều này được cung cấp bởi phần quan trọng. Và đồng ý, sắp xếp lại bên trong phần quan trọng là không sai. Tuy nhiên, tôi có cần phải bọc thiết lập của 'threadParameterAbort_' trong luồng GUI với một phần không chính xác? Dường như dư thừa vì bản thân hoạt động đó là nguyên tử, phải không? – gablin

9

Wikipedia nói nó khá tốt.

Trong C, và hậu quả là C++, từ khóa dễ bay hơi được dự định để cho phép truy cập đến các thiết bị bộ nhớ ánh xạ cho phép sử dụng các biến giữa setjmp cho phép sử dụng các biến sig_atomic_t trong xử lý tín hiệu

Các thao tác trên các biến dễ bay hơi là không phải nguyên tử cũng không thiết lập mối quan hệ xảy ra trước thích hợp cho luồng . Điều này theo các tiêu chuẩn có liên quan (C, C++, POSIX, WIN32), và đây là vấn đề thực tế cho phần lớn các triển khai hiện tại hiện tại. Từ khóa dễ bay hơi về cơ bản là vô giá trị như một cấu trúc luồng di động .

+4

Tôi không hiểu tại sao mọi người cứ suy nghĩ 'bay hơi' cung cấp bất kỳ loại an toàn chủ đề nào. +1 –

+0

Họ đến từ Java và thậm chí ở đó nó không có đảm bảo an toàn thread. – wheaties

+4

Wikipedia là một nguồn trích dẫn khủng khiếp (Ngay cả người sáng lập Wikipedias cũng không sử dụng wikipedia làm nguồn trích dẫn). Sử dụng nó như là một điểm khởi đầu để làm nghiên cứu của bạn nhưng ít nhất là trích dẫn một nguồn có thẩm quyền. –

3

volatile không cần thiết cũng không đủ để đa luồng trong C++. Nó vô hiệu hóa tối ưu hóa được chấp nhận hoàn toàn, nhưng không thực thi những thứ như nguyên tử cần thiết.

Chỉnh sửa: thay vì sử dụng phần quan trọng, tôi có thể sử dụng InterlockedIncrement, cho phép ghi nguyên tử với chi phí thấp hơn.

Những gì tôi thường làm, tuy nhiên, đang treo lên một hàng đợi an toàn chủ đề (hoặc deque) làm đầu vào cho chuỗi. Khi bạn có một cái gì đó cho thread để làm, bạn chỉ cần đặt một gói dữ liệu mô tả công việc vào hàng đợi, và thread nào nó khi nó có thể. Khi bạn muốn các chủ đề để tắt bình thường, bạn đặt một gói "tắt máy" vào hàng đợi. Nếu bạn cần hủy bỏ ngay lập tức, bạn sử dụng deque thay vào đó, và đặt lệnh "hủy bỏ" ở phía trước của deque. Về lý thuyết, điều này có thiếu sót rằng nó không hủy bỏ thread cho đến khi nó kết thúc nhiệm vụ hiện tại của nó. Về tất cả những điều đó có nghĩa là bạn muốn giữ cho mỗi tác vụ gần bằng cùng một phạm vi kích thước/độ trễ như tần suất mà bạn hiện đang kiểm tra cờ.

Thiết kế chung này tránh được toàn bộ các sự cố IPC.

+0

OK; Bạn sẽ phải mở rộng điều đó. Tôi đồng ý rằng nó không đủ. Tôi đồng ý rằng sự phụ thuộc tính di động trên dễ bay hơi sẽ không hữu ích. Nhưng nó sẽ không cung cấp một số lợi ích trên Windows bằng cách sử dụng Cl1 trong đó bộ nhớ không được lưu trữ và do đó chúng tôi không cần phải lo lắng về vấn đề kết hợp bộ nhớ cache trên nhiều luồng. –

+0

@ Jerry Quan tài: Một lần nữa, tôi nhận thức rõ rằng 'dễ bay hơi' không đảm bảo an toàn luồng hoặc nguyên tử. Ngoài ra, không phải là viết hoặc đọc một giá trị 'bool' một hoạt động nguyên tử? – gablin

+0

@Martin: 'volatile' chỉ ngăn trình biên dịch lưu dữ liệu trong sổ đăng ký; nó * không có gì * để ngăn chặn/trợ giúp với các vấn đề kết hợp bộ nhớ cache. @gablin: đọc hoặc viết một 'bool' có thể là nguyên tử - sau đó một lần nữa, nó có thể không. Nó có lẽ thường sẽ được mặc định với MS VC++, nhưng nó chắc chắn có thể tạo ra một 'bool' mà sẽ không được đọc/viết nguyên tử. –

0

Trong khi câu trả lời khác là chính xác, tôi cũng đề nghị bạn hãy xem các "Microsoft cụ thể" của MSDN documentation for volatile

+0

Nó thực sự trông rất giống với những gì tôi đã áp dụng trong chương trình của tôi. Nhưng tôi vẫn nên sử dụng một phần quan trọng? Nhu cầu của nó có được bảo đảm trong chương trình của tôi không? – gablin

+0

Không cần phải trả lời câu hỏi đó (xem thảo luận về một trong các câu trả lời khác). – gablin

0

Tôi nghĩ rằng nó sẽ làm việc tốt (nguyên tử hay không) bởi vì bạn chỉ sử dụng nó để hủy bỏ một thao tác nền.

Điều chính mà dễ bay hơi sẽ làm là ngăn biến được lưu trong bộ nhớ cache hoặc truy xuất từ ​​sổ đăng ký. Bạn có thể chắc chắn rằng nó đến từ bộ nhớ chính.

Vì vậy, nếu tôi là bạn, vâng tôi sẽ xác định biến là dễ bay hơi.

Tôi nghĩ tốt nhất là giả định rằng biến động là cần thiết để đảm bảo biến khi được viết và sau đó đọc bởi các chủ đề khác mà chúng thực sự nhận được giá trị chính xác cho biến đó càng sớm càng tốt và đảm bảo rằng IF không được tối ưu hóa đi (mặc dù tôi nghi ngờ nó sẽ là).

+0

Nếu đọc và ghi vào 'threadParamAbort_' thực sự không phải là nguyên tử, thì tôi cũng sẽ cần phải lo lắng về nó trên một hệ thống CPU đơn lẻ. Và như tôi đã chỉ ra cho tất cả mọi người, tôi * biết * rằng 'bay hơi' không cung cấp nguyên tử cho bất kỳ hoạt động nào; vì vậy chúng ta có thể dừng việc chỉ ra điều đó với tôi không? – gablin

+0

Giải quyết và lắng nghe. Tôi đã viết điều này ban đầu 2-3 ngày trước vì vậy không cần phải nói bất cứ điều gì với tôi. Của tôi không phải là một câu trả lời mới ok? Nguyên tử là một khác biệt nhưng liên quan đến an toàn luồng mà là tất cả những gì bạn đã đề cập trong câu hỏi của bạn. Bây giờ tôi đã cập nhật phản hồi của tôi sau khi suy nghĩ thêm về những gì bạn đang cần. – Matt

1

Dường như trường hợp sử dụng tương tự bị hủy ở đây: volatile - Multithreaded Programmer's Best Friend bởi Alexandrescu. Nó nói rằng chính xác trong trường hợp này (để tạo cờ) volatile có thể được sử dụng một cách hoàn hảo.

Vì vậy, có chính xác trong trường hợp này mã phải chính xác. volative sẽ ngăn chặn cả hai - đọc từ bộ nhớ cache và ngăn trình biên dịch tối ưu hóa tuyên bố if.

0

Ok, vì vậy bạn đã bị đánh đập đầy đủ về an toàn ổn định và chủ đề !, nhưng ...

Một ví dụ cho mã cụ thể của bạn (mặc dù một căng và trong vòng kiểm soát của bạn) là một thực tế rằng bạn đang nhìn vào nhiều lần biến của bạn trong một 'giao dịch':

if ((threadParamAbort_ != NULL) && *threadParamAbort_)

Nếu vì bất cứ lý do threadParamAbort_ sẽ bị xóa sau khi phía bên tay trái và trước phía bên tay phải sau đó bạn sẽ tới đích của một con trỏ bị xóa. Một lần nữa, điều này là không thể cho bạn có quyền kiểm soát nhưng là một ví dụ về những gì dễ bay hơi và nguyên tử không thể làm cho bạn.

+0

Vâng, đây là một quan sát chính xác, nhưng tôi biết rằng 'threadParamAbort_' sẽ không bao giờ bị xóa khi loại bỏ dòng mã này vì mã của tôi được cấu trúc như vậy. Trừ khi có một lỗi, tất nhiên, nhưng đó là một vấn đề hoàn toàn khác nhau. – gablin

3

Đối với câu trả lời của tôi cho câu hỏi của ngày hôm qua, không, volatile là không cần thiết. Trong thực tế, đa luồng ở đây là không liên quan.

while (readLine(&line)) { // threadParamAbort_ is not local: 
     if ((threadParamAbort_ != NULL) && *threadParamAbort_) { 
  1. ngăn chặn việc đọc * threadParameterAbort_ sử dụng bộ nhớ cache và thay vào đó lấy giá trị từ bộ nhớ , và
  2. ngăn chặn các trình biên dịch từ loại bỏ các mệnh đề if trong các sợi nhân do tối ưu hóa .

readLine là mã thư viện bên ngoài, nếu không nó sẽ gọi mã thư viện bên ngoài. Do đó, trình biên dịch không thể giả định có bất kỳ biến không phải là địa phương nào mà nó không sửa đổi. Khi một con trỏ tới một đối tượng (hoặc siêu đối tượng của nó) đã được hình thành, nó có thể được truyền và lưu trữ ở bất cứ đâu. Trình biên dịch không thể theo dõi những gì con trỏ kết thúc trong các biến toàn cầu và không.

Vì vậy, trình biên dịch giả định rằng readLine có riêng riêng static bool * để threadParamAbort_ và sửa đổi giá trị. Vì vậy, nó cần thiết để tải lại từ bộ nhớ.

+0

Tôi hiểu. Cảm ơn vì đã làm rõ điều đó. – gablin

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