Khi nào sử dụng dễ bay hơi với đa luồng?


130

Nếu có hai luồng truy cập vào một biến toàn cục thì nhiều hướng dẫn nói làm cho biến đó biến động để ngăn trình biên dịch lưu bộ đệm vào biến trong một thanh ghi và do đó nó không được cập nhật chính xác. Tuy nhiên, hai luồng cả hai truy cập vào một biến được chia sẻ là thứ cần được bảo vệ thông qua một mutex phải không? Nhưng trong trường hợp đó, giữa khóa luồng và giải phóng mutex, mã nằm trong một phần quan trọng trong đó chỉ có một luồng có thể truy cập vào biến, trong trường hợp nào biến không cần phải biến động?

Vì vậy, việc sử dụng / mục đích của biến động trong một chương trình đa luồng là gì?


3
Trong một số trường hợp, bạn không muốn / cần được bảo vệ bởi mutex.
Stefan Mai

4
Đôi khi nó tốt để có một điều kiện chủng tộc, đôi khi không. Làm thế nào bạn đang sử dụng biến này?
David Heffernan

3
@David: Một ví dụ về thời điểm "ổn" khi có một cuộc đua, làm ơn?
John Dibling

6
@ John đi đây. Hãy tưởng tượng bạn có một luồng công nhân đang xử lý một số nhiệm vụ. Các luồng công nhân tăng một bộ đếm bất cứ khi nào nó hoàn thành một nhiệm vụ. Chủ đề định kỳ đọc bộ đếm này và cập nhật cho người dùng tin tức về tiến trình. Miễn là bộ đếm được căn chỉnh chính xác để tránh bị rách, không cần phải đồng bộ hóa truy cập. Mặc dù có một chủng tộc, nó là lành tính.
David Heffernan

5
@John Phần cứng mà mã này chạy đảm bảo rằng các biến được căn chỉnh có thể bị rách. Nếu công nhân đang cập nhật n thành n + 1 khi người đọc đọc, thì người đọc không quan tâm họ có nhận được n hay n + 1 hay không. Không có quyết định quan trọng sẽ được thực hiện vì nó chỉ được sử dụng để báo cáo tiến độ.
David Heffernan

Câu trả lời:


167

Câu trả lời ngắn & nhanh : volatilelà (gần như) vô dụng đối với nền tảng ứng dụng đa nền tảng, lập trình đa ứng dụng. Nó không cung cấp bất kỳ sự đồng bộ hóa nào, nó không tạo ra hàng rào bộ nhớ và cũng không đảm bảo thứ tự thực hiện các hoạt động. Nó không làm cho hoạt động nguyên tử. Nó không làm cho mã của bạn một cách kỳ diệu an toàn. volatilecó thể là cơ sở bị hiểu lầm nhiều nhất trong tất cả C ++. Xem cái này , cái nàycái này để biết thêm thông tin vềvolatile

Mặt khác, volatilecó một số sử dụng có thể không quá rõ ràng. Nó có thể được sử dụng nhiều theo cùng một cách mà người ta sẽ sử dụng constđể giúp trình biên dịch chỉ cho bạn biết nơi bạn có thể đang mắc lỗi khi truy cập một số tài nguyên được chia sẻ theo cách không được bảo vệ. Việc sử dụng này được Alexandrescu thảo luận trong bài viết này . Tuy nhiên, về cơ bản, đây là sử dụng hệ thống loại C ++ theo cách thường được xem là một kế hoạch và có thể gợi lên Hành vi không xác định.

volatileđược dành riêng để sử dụng khi giao tiếp với phần cứng được ánh xạ bộ nhớ, bộ xử lý tín hiệu và hướng dẫn mã máy setjmp. Điều này giúp volatileáp dụng trực tiếp cho lập trình cấp hệ thống thay vì lập trình cấp ứng dụng thông thường.

Tiêu chuẩn C ++ 2003 không nói rằng volatileáp dụng bất kỳ loại ngữ nghĩa Mua hoặc Phát hành nào cho các biến. Trên thực tế, Standard hoàn toàn im lặng trong mọi vấn đề về đa luồng. Tuy nhiên, các nền tảng cụ thể áp dụng ngữ nghĩa Mua và Phát hành trên volatilecác biến.

[Cập nhật cho C ++ 11]

C ++ 11 tiêu chuẩn hiện nay không thừa nhận đa luồng trực tiếp trong mô hình bộ nhớ và các lanuage, và nó cung cấp cơ sở vật chất thư viện để đối phó với nó một cách nền tảng độc lập. Tuy nhiên, ngữ nghĩa của volatilevẫn không thay đổi. volatilevẫn không phải là một cơ chế đồng bộ hóa. Bjarne Stroustrup nói nhiều như vậy trong TCPPPL4E:

Không sử dụng volatilengoại trừ mã cấp thấp liên quan trực tiếp đến phần cứng.

Đừng cho rằng volatilecó ý nghĩa đặc biệt trong mô hình bộ nhớ. Nó không. Nó không phải - như trong một số ngôn ngữ sau này - một cơ chế đồng bộ hóa. Để có được đồng bộ hóa, sử dụng atomic, a mutexhoặc a condition_variable.

[/ Kết thúc cập nhật]

Trên đây, tất cả đều áp dụng ngôn ngữ C ++, như được định nghĩa bởi Tiêu chuẩn 2003 (và bây giờ là Tiêu chuẩn 2011). Tuy nhiên, một số nền tảng cụ thể có thêm chức năng hoặc hạn chế bổ sung cho những gì volatilekhông. Ví dụ, trong MSVC 2010 (ít nhất) tiếp thu và phát hành ngữ nghĩa làm áp dụng đối với một số hoạt động trên volatilecác biến. Từ MSDN :

Khi tối ưu hóa, trình biên dịch phải duy trì thứ tự giữa các tham chiếu đến các đối tượng dễ bay hơi cũng như các tham chiếu đến các đối tượng toàn cầu khác. Đặc biệt,

Một ghi vào một đối tượng dễ bay hơi (viết dễ bay hơi) có ngữ nghĩa phát hành; một tham chiếu đến một đối tượng toàn cục hoặc tĩnh xảy ra trước khi ghi vào một đối tượng dễ bay hơi trong chuỗi lệnh sẽ xảy ra trước khi ghi biến động đó trong nhị phân được biên dịch.

Việc đọc một đối tượng dễ bay hơi (đọc dễ bay hơi) có được ngữ nghĩa; một tham chiếu đến một đối tượng toàn cục hoặc tĩnh xảy ra sau khi đọc bộ nhớ dễ bay hơi trong chuỗi lệnh sẽ xảy ra sau khi đọc biến động đó trong tệp nhị phân được biên dịch.

Tuy nhiên, bạn có thể lưu ý rằng nếu bạn theo liên kết trên, sẽ có một số tranh luận trong các ý kiến ​​về việc liệu có thu được / phát hành ngữ nghĩa thực sự áp dụng trong trường hợp này hay không.


19
Một phần trong tôi muốn hạ thấp điều này vì giai điệu hạ thấp của câu trả lời và bình luận đầu tiên. "Không ổn định là vô dụng" giống như "cấp phát bộ nhớ thủ công là vô ích". Nếu bạn có thể viết một chương trình đa luồng mà không có volatilenó là do bạn đứng trên vai của những người đã từng volatilethực hiện các thư viện luồng.
Ben Jackson

19
@Ben chỉ vì điều gì đó thách thức niềm tin của bạn không khiến nó bị hạ thấp
David Heffernan

38
@Ben: không, hãy đọc những gì volatilethực sự làm trong C ++. Những gì @John nói là chính xác , kết thúc câu chuyện. Nó không liên quan gì đến mã ứng dụng so với mã thư viện, hoặc "bình thường" so với "lập trình viên toàn tri giống như thần" cho vấn đề đó. volatilelà không cần thiết và vô dụng để đồng bộ giữa các chủ đề. Thư viện luồng không thể được thực hiện về mặt volatile; dù sao nó cũng phải dựa vào các chi tiết cụ thể của nền tảng và khi bạn dựa vào những chi tiết đó, bạn không còn cần nữa volatile.
jalf

6
@jalf: "dễ bay hơi là không cần thiết và vô dụng để đồng bộ hóa giữa các luồng" (đó là những gì bạn nói) không giống như "dễ bay hơi là vô dụng đối với lập trình đa luồng" (đó là những gì John nói trong câu trả lời). Bạn đúng 100%, nhưng tôi không đồng ý với John (một phần) - vẫn có thể sử dụng biến động cho lập trình đa luồng (đối với một nhóm tác vụ rất hạn chế)

4
@GMan: Mọi thứ hữu ích chỉ hữu ích trong một tập hợp các yêu cầu hoặc điều kiện nhất định. Volility rất hữu ích cho lập trình đa luồng trong một tập hợp các điều kiện nghiêm ngặt (và trong một số trường hợp, thậm chí có thể tốt hơn (đối với một số định nghĩa tốt hơn) so với các lựa chọn thay thế). Bạn nói "bỏ qua điều này và .." nhưng trường hợp khi biến động là hữu ích cho đa luồng không bỏ qua bất cứ điều gì. Bạn đã tạo nên một cái gì đó mà tôi không bao giờ tuyên bố. Đúng, tính hữu dụng của tính dễ bay hơi bị hạn chế, nhưng nó tồn tại - nhưng tất cả chúng ta đều có thể đồng ý rằng nó KHÔNG hữu ích cho việc đồng bộ hóa.

31

(Lưu ý của biên tập viên: trong C ++ 11 volatilekhông phải là công cụ phù hợp cho công việc này và vẫn có UB cuộc đua dữ liệu. Sử dụng std::atomic<bool>với std::memory_order_relaxedtải / cửa hàng để thực hiện việc này mà không cần UB. Trên các triển khai thực tế, nó sẽ biên dịch giống như volatile. Tôi đã thêm một câu trả lời chi tiết hơn và cũng giải quyết các quan niệm sai lầm trong các nhận xét rằng bộ nhớ được sắp xếp yếu có thể là một vấn đề đối với trường hợp sử dụng này: tất cả các CPU trong thế giới thực đều có bộ nhớ chia sẻ mạch lạc nên volatilesẽ hoạt động cho việc triển khai C ++ thực này. Không làm điều đó.

Một số cuộc thảo luận trong các bình luận dường như đang nói về các trường hợp sử dụng khác, trong đó bạn sẽ cần thứ gì đó mạnh hơn nguyên tử thoải mái. Câu trả lời này đã chỉ ra rằng không volatilecho phép bạn đặt hàng.)


Dễ bay hơi đôi khi hữu ích vì lý do sau: mã này:

/* global */ bool flag = false;

while (!flag) {}

được tối ưu hóa bởi gcc để:

if (!flag) { while (true) {} }

Điều này rõ ràng là không chính xác nếu cờ được viết bởi chủ đề khác. Lưu ý rằng nếu không có tối ưu hóa này, cơ chế đồng bộ hóa có thể hoạt động (tùy thuộc vào mã khác, một số rào cản bộ nhớ có thể cần thiết) - không cần có mutex trong 1 nhà sản xuất - 1 kịch bản tiêu dùng.

Mặt khác, từ khóa dễ bay hơi quá kỳ lạ để có thể sử dụng được - nó không cung cấp bất kỳ thứ tự bộ nhớ nào đảm bảo truy cập cả hai truy cập dễ bay hơi và không bay hơi và không cung cấp bất kỳ hoạt động nguyên tử nào - tức là bạn không nhận được trợ giúp từ trình biên dịch với từ khóa bị vô hiệu hóa .


4
Nếu tôi nhớ lại, nguyên tử C ++ 0x, có nghĩa là làm đúng những gì mà nhiều người tin (không chính xác) được thực hiện bởi sự dễ bay hơi.
David Heffernan

13
volatilekhông ngăn chặn truy cập bộ nhớ được sắp xếp lại. volatiletruy cập sẽ không được sắp xếp lại theo sự tôn trọng lẫn nhau, nhưng chúng không đảm bảo về việc sắp xếp lại đối với các volatileđối tượng không , và vì vậy, về cơ bản, chúng cũng vô dụng như cờ.
jalf

13
@Ben: Tôi nghĩ bạn đã làm nó lộn ngược. Đám đông "dễ bay hơi là vô dụng" phụ thuộc vào thực tế đơn giản là chất dễ bay hơi không bảo vệ chống lại sự sắp xếp lại , điều đó có nghĩa là nó hoàn toàn vô dụng đối với việc đồng bộ hóa. Các cách tiếp cận khác có thể vô dụng như nhau (như bạn đã đề cập, tối ưu hóa mã thời gian liên kết có thể cho phép trình biên dịch xem mã mà bạn cho rằng trình biên dịch sẽ coi là hộp đen), nhưng điều đó không khắc phục được các thiếu sót volatile.
jalf

15
@jalf: Xem bài viết của Arch Robinson (được liên kết ở nơi khác trên trang này), bình luận thứ 10 (bởi "Spud"). Về cơ bản, việc sắp xếp lại không thay đổi logic của mã. Mã được đăng sử dụng cờ để hủy tác vụ (thay vì để báo hiệu nhiệm vụ được thực hiện), do đó, không có vấn đề gì nếu tác vụ bị hủy trước hoặc sau mã (ví dụ: while (work_left) { do_piece_of_work(); if (cancel) break;}nếu việc hủy được sắp xếp lại trong vòng lặp, logic vẫn hợp lệ. Tôi có một đoạn mã hoạt động tương tự: nếu luồng chính muốn chấm dứt, nó sẽ đặt cờ cho các luồng khác, nhưng nó không ...

15
... vấn đề là nếu các luồng khác thực hiện thêm một vài lần lặp các vòng lặp công việc trước khi chúng kết thúc, miễn là điều đó xảy ra một cách hợp lý ngay sau khi cờ được đặt. Tất nhiên, đây là cách sử dụng DUY NHẤT mà tôi có thể nghĩ ra và khá phù hợp (và có thể không hoạt động trên các nền tảng mà việc ghi vào biến dễ bay hơi không làm thay đổi hiển thị cho các luồng khác, mặc dù trên ít nhất là x86 và x86-64 làm). Tôi chắc chắn sẽ không khuyên bất cứ ai thực sự làm điều đó mà không có lý do chính đáng, tôi chỉ nói rằng một tuyên bố về chăn như "dễ bay hơi là KHÔNG BAO GIỜ hữu ích trong mã đa luồng" không đúng 100%.

15

Trong C ++ 11, thông thường không bao giờ sử dụng volatilecho luồng, chỉ dành cho MMIO

Nhưng TL: DR, nó "hoạt động" giống như nguyên tử với mo_relaxedphần cứng với bộ nhớ kết hợp (nghĩa là mọi thứ); Nó là đủ để ngăn chặn trình biên dịch giữ vars trong sổ đăng ký. atomickhông cần các rào cản bộ nhớ để tạo khả năng hiển thị nguyên tử hoặc liên luồng, chỉ để làm cho luồng hiện tại chờ trước / sau một thao tác để tạo thứ tự giữa các luồng này truy cập vào các biến khác nhau. mo_relaxedkhông bao giờ cần bất kỳ rào cản, chỉ cần tải, lưu trữ, hoặc RMW.

Đối với các nguyên tử cuộn của riêng bạn với volatile(và nội tuyến cho các rào cản) trong những ngày xưa tồi tệ trước C ++ 11 std::atomic, volatilelà cách tốt duy nhất để làm cho một số thứ hoạt động . Nhưng nó phụ thuộc vào rất nhiều giả định về cách thức triển khai hoạt động và không bao giờ được đảm bảo bởi bất kỳ tiêu chuẩn nào.

Ví dụ, nhân Linux vẫn sử dụng các nguyên tử cuộn bằng tay của riêng mình volatile, nhưng chỉ hỗ trợ một vài triển khai C cụ thể (GNU C, clang và có thể cả ICC). Một phần là do các phần mở rộng GNU C và cú pháp và ngữ nghĩa asm nội tuyến, nhưng cũng vì nó phụ thuộc vào một số giả định về cách trình biên dịch hoạt động.

Nó hầu như luôn luôn là lựa chọn sai cho các dự án mới; bạn có thể sử dụng std::atomic(với std::memory_order_relaxed) để có được trình biên dịch để phát ra mã máy hiệu quả giống như bạn có thể làm với volatile. std::atomicvới mo_relaxedlỗi thời volatilecho mục đích luồng. (ngoại trừ có thể khắc phục các lỗi tối ưu hóa bị bỏ lỡ với atomic<double>một số trình biên dịch .)

Việc triển khai nội bộ std::atomictrên các trình biên dịch chính (như gcc và clang) không chỉ sử dụng volatilenội bộ; trình biên dịch trực tiếp phơi bày tải nguyên tử, lưu trữ và các hàm dựng sẵn của RMW. (ví dụ như GNU C __atomicbuiltins mà hoạt động trên các đối tượng "đồng bằng".)


Dễ bay hơi là có thể sử dụng trong thực tế (nhưng không làm điều đó)

Điều đó nói rằng, volatilecó thể sử dụng trong thực tế cho những thứ như exit_nowcờ trên tất cả (?) Việc triển khai C ++ hiện có trên CPU thực, do cách thức hoạt động của CPU (bộ nhớ kết hợp) và các giả định được chia sẻ về cách volatilehoạt động. Nhưng không nhiều, và không được khuyến khích. Mục đích của câu trả lời này là để giải thích cách thức triển khai CPU và C ++ hiện tại thực sự hoạt động. Nếu bạn không quan tâm đến điều đó, tất cả những gì bạn cần biết là std::atomicvới các lỗi thời của mo_relaxed volatilecho luồng.

(Tiêu chuẩn ISO C ++ khá mơ hồ về nó, chỉ nói rằng các volatiletruy cập nên được đánh giá đúng theo quy tắc của máy trừu tượng C ++, không được tối ưu hóa. Do việc triển khai thực tế sử dụng không gian địa chỉ bộ nhớ của máy để mô hình hóa không gian địa chỉ C ++, điều này có nghĩa là các lần volatileđọc và bài tập phải biên dịch để tải / lưu các lệnh để truy cập biểu diễn đối tượng trong bộ nhớ.)


Như một câu trả lời khác chỉ ra, một exit_nowlá cờ là một trường hợp đơn giản của giao tiếp giữa các luồng không cần bất kỳ sự đồng bộ hóa nào : nó không xuất bản rằng nội dung mảng đã sẵn sàng hoặc bất cứ điều gì tương tự. Chỉ cần một cửa hàng được chú ý kịp thời bởi một tải không được tối ưu hóa trong một luồng khác.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Không có tính dễ bay hơi hoặc nguyên tử, quy tắc as-if và giả định không có cuộc đua dữ liệu UB cho phép trình biên dịch tối ưu hóa nó thành asm chỉ kiểm tra cờ một lần , trước khi vào (hoặc không) một vòng lặp vô hạn. Đây chính xác là những gì xảy ra trong cuộc sống thực cho các trình biên dịch thực. (Và thường tối ưu hóa đi nhiều do_stuffvì vòng lặp không bao giờ thoát, do đó, bất kỳ mã nào sau này có thể đã sử dụng kết quả đều không thể truy cập được nếu chúng ta nhập vòng lặp).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Chương trình đa luồng bị kẹt trong chế độ tối ưu hóa nhưng chạy bình thường trong -O0 là một ví dụ (với mô tả về đầu ra asm của GCC) về cách chính xác điều này xảy ra với GCC trên x86-64. Ngoài ra lập trình MCU - Tối ưu hóa C ++ O2 bị phá vỡ trong khi vòng lặp trên thiết bị điện tử.SE cho thấy một ví dụ khác.

Chúng tôi thường muốn tối ưu hóa mạnh mẽ rằng CSE và Palăng tải ra khỏi các vòng lặp, bao gồm cả các biến toàn cục.

Trước C ++ 11, volatile bool exit_nowlà một cách để làm cho công việc này như dự định (trên các triển khai C ++ bình thường). Nhưng trong C ++ 11, UB cuộc đua dữ liệu vẫn áp dụng để volatilenó không thực sự được đảm bảo bởi tiêu chuẩn ISO để hoạt động ở mọi nơi, ngay cả khi giả sử bộ đệm kết hợp CTNH.

Lưu ý rằng đối với các loại rộng hơn, volatilekhông đảm bảo thiếu rách. Tôi đã bỏ qua sự khác biệt đó ở đây boolvì nó không phải là vấn đề đối với việc triển khai bình thường. Nhưng đó cũng là một phần lý do tại sao volatilevẫn phải chịu cuộc đua dữ liệu UB thay vì tương đương với nguyên tử thoải mái.

Lưu ý rằng "như dự định" không có nghĩa là luồng đang exit_nowchờ đợi luồng khác thực sự thoát. Hoặc thậm chí là nó chờ cho exit_now=truecửa hàng dễ bay hơi thậm chí có thể nhìn thấy trên toàn cầu trước khi tiếp tục các hoạt động sau này trong chuỗi này. ( atomic<bool>với mặc định mo_seq_cstsẽ khiến nó chờ trước khi tải seq_cst sau ít nhất. Trên nhiều ISA bạn sẽ nhận được một rào cản đầy đủ sau cửa hàng).

C ++ 11 cung cấp một cách không phải UB để biên dịch giống nhau

Một "tiếp tục chạy" hoặc "thoát ngay bây giờ" cờ nên sử dụng std::atomic<bool> flagvớimo_relaxed

Sử dụng

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

sẽ cung cấp cho bạn chính xác mã asm (không có hướng dẫn rào cản đắt tiền) mà bạn nhận được volatile flag.

Cũng như không bị rách, atomiccũng cung cấp cho bạn khả năng lưu trữ trong một luồng và tải trong một luồng khác mà không cần UB, vì vậy trình biên dịch không thể kéo tải ra khỏi vòng lặp. (Giả định không có cuộc đua dữ liệu UB là những gì cho phép tối ưu hóa mạnh mẽ mà chúng ta muốn cho các vật thể không bay hơi không nguyên tử.) Tính năng atomic<T>này khá giống với những gì volatileđối với tải thuần túy và các cửa hàng thuần túy.

atomic<T>cũng thực hiện +=các hoạt động của RMW nguyên tử (đắt hơn đáng kể so với tải nguyên tử vào tạm thời, vận hành, sau đó là một cửa hàng nguyên tử riêng biệt. Nếu bạn không muốn có một RMW nguyên tử, hãy viết mã của bạn với tạm thời cục bộ).

Với seq_cstthứ tự mặc định bạn nhận được while(!flag), nó cũng thêm đơn đặt hàng đảm bảo wrt. truy cập phi nguyên tử, và truy cập nguyên tử khác.

(Về lý thuyết, tiêu chuẩn ISO C ++ không loại trừ tối ưu hóa nguyên tử thời gian biên dịch. Nhưng trong các trình biên dịch thực tế thì không bởi vì không có cách nào để kiểm soát khi điều đó sẽ không ổn. Có một vài trường hợp thậm chí volatile atomic<T>có thể không Có đủ quyền kiểm soát tối ưu hóa nguyên tử nếu trình biên dịch đã tối ưu hóa, vì vậy hiện tại trình biên dịch không. Xem tại sao trình biên dịch không hợp nhất std :: Atomic write ? Lưu ý rằng wg21 / p0062 khuyên bạn không nên sử dụng volatile atomicmã hiện tại để bảo vệ chống tối ưu hóa nguyên tử.)


volatile thực sự hoạt động cho điều này trên các CPU thực (nhưng vẫn không sử dụng nó)

ngay cả với các mô hình bộ nhớ được sắp xếp yếu (không phải x86) . Nhưng không thực sự sử dụng nó, sử dụng atomic<T>với mo_relaxedthay !! Điểm của phần này là để giải quyết những quan niệm sai lầm về cách thức hoạt động của CPU thực sự, chứ không phải để biện minh volatile. Nếu bạn đang viết mã không khóa, có lẽ bạn quan tâm đến hiệu suất. Hiểu về bộ nhớ cache và chi phí liên lạc giữa các luồng thường rất quan trọng để có hiệu suất tốt.

CPU thực có bộ nhớ kết hợp / bộ nhớ chia sẻ: sau khi một cửa hàng từ một lõi trở nên hiển thị trên toàn cầu, không có lõi nào khác có thể tải một giá trị cũ. (Xem thêm Các lập trình viên Huyền thoại Tin tưởng về Bộ nhớ CPU nói về một số biến động Java, tương đương với C ++ atomic<T>với thứ tự bộ nhớ seq_cst.)

Khi tôi nói tải , tôi có nghĩa là một lệnh asm truy cập bộ nhớ. Đó là những gì một volatiletruy cập đảm bảo, và không giống như chuyển đổi từ giá trị sang giá trị của biến C ++ không nguyên tử / không bay hơi. (ví dụ local_tmp = flaghoặc while(!flag)).

Điều duy nhất bạn cần đánh bại là tối ưu hóa thời gian biên dịch hoàn toàn không tải lại sau lần kiểm tra đầu tiên. Bất kỳ tải + kiểm tra trên mỗi lần lặp là đủ, không có bất kỳ thứ tự. Không có sự đồng bộ giữa luồng này và luồng chính, sẽ không có ý nghĩa gì khi nói về việc chính xác cửa hàng xảy ra hoặc đặt hàng tải wrt. các hoạt động khác trong vòng lặp. Chỉ khi nó hiển thị với chủ đề này là những gì quan trọng. Khi bạn thấy cờ exit_now được đặt, bạn thoát. Độ trễ giữa các lõi trên Xeon x86 điển hình có thể là khoảng 40ns giữa các lõi vật lý riêng biệt .


Về lý thuyết: Các luồng C ++ trên phần cứng không có bộ đệm kết hợp

Tôi không thấy bất kỳ cách nào điều này có thể hiệu quả từ xa, chỉ với ISO C ++ thuần túy mà không yêu cầu lập trình viên thực hiện các thao tác rõ ràng trong mã nguồn.

Về lý thuyết, bạn có thể có một triển khai C ++ trên một máy không như thế này, yêu cầu các luồng rõ ràng do trình biên dịch tạo để làm cho mọi thứ hiển thị với các luồng khác trên các lõi khác . (Hoặc để đọc để không sử dụng bản sao có thể cũ). Chuẩn C ++ không biến điều này thành không thể, nhưng mô hình bộ nhớ của C ++ được thiết kế xoay quanh hiệu quả trên các máy nhớ chia sẻ kết hợp. Ví dụ, tiêu chuẩn C ++ thậm chí còn nói về "kết hợp đọc-đọc", "kết hợp đọc-đọc", v.v. Một lưu ý trong tiêu chuẩn thậm chí còn chỉ ra kết nối với phần cứng:

http://eel.is/c++draft/intro.races#19

[Lưu ý: Bốn yêu cầu kết hợp trước đó không cho phép trình biên dịch sắp xếp lại các hoạt động nguyên tử thành một đối tượng duy nhất, ngay cả khi cả hai hoạt động đều được tải thoải mái. Điều này có hiệu quả làm cho bảo đảm kết hợp bộ đệm được cung cấp bởi hầu hết các phần cứng có sẵn cho các hoạt động nguyên tử của C ++. - lưu ý cuối]

Không có cơ chế nào cho một releasecửa hàng chỉ tự tuôn ra và một vài dải địa chỉ được chọn: nó sẽ phải đồng bộ hóa mọi thứ vì nó không biết những luồng nào khác có thể muốn đọc nếu tải của họ thấy cửa hàng phát hành này (tạo thành một trình tự phát hành thiết lập mối quan hệ xảy ra trước các chủ đề, đảm bảo rằng các hoạt động phi nguyên tử trước đó được thực hiện bởi luồng viết hiện an toàn để đọc. Trừ khi nó viết thêm cho chúng sau kho lưu trữ phát hành ...) Hoặc trình biên dịch sẽ có để thực sự thông minh để chứng minh rằng chỉ cần một vài dòng bộ đệm cần xả.

Liên quan: câu trả lời của tôi về Mov + mfence có an toàn trên NUMA không? đi sâu vào chi tiết về sự không tồn tại của các hệ thống x86 mà không có bộ nhớ chia sẻ mạch lạc. Cũng liên quan: Tải và lưu trữ sắp xếp lại trên ARM để biết thêm về tải / lưu trữ đến cùng một vị trí.

những tôi nghĩ rằng các cụm với bộ nhớ chia sẻ không kết hợp, nhưng chúng không phải là các máy ảnh đơn hệ thống. Mỗi miền kết hợp chạy một hạt nhân riêng biệt, vì vậy bạn không thể chạy các luồng của một chương trình C ++ duy nhất trên nó. Thay vào đó, bạn chạy các phiên bản riêng biệt của chương trình (mỗi phiên bản có không gian địa chỉ riêng: các con trỏ trong một phiên bản không hợp lệ trong trường hợp khác).

Để khiến họ liên lạc với nhau thông qua các lần xóa rõ ràng, bạn thường sử dụng MPI hoặc API chuyển tin nhắn khác để làm cho chương trình chỉ định phạm vi địa chỉ nào cần xóa.


Phần cứng thực không chạy std::threadqua ranh giới kết hợp bộ đệm:

Một số chip ARM không đối xứng tồn tại, với không gian địa chỉ vật lý dùng chung nhưng không có miền bộ nhớ cache có thể chia sẻ bên trong. Vì vậy, không mạch lạc. (ví dụ: luồng nhận xét một lõi A8 và Cortex-M3 như TI Sitara AM335x).

Nhưng các hạt nhân khác nhau sẽ chạy trên các lõi đó, không phải là một hình ảnh hệ thống duy nhất có thể chạy các luồng trên cả hai lõi. Tôi không biết về bất kỳ triển khai C ++ nào chạy các std::threadluồng trên lõi CPU mà không có bộ nhớ kết hợp.

Đối với ARM cụ thể, GCC và clang tạo mã giả sử tất cả các luồng chạy trong cùng một miền có thể chia sẻ bên trong. Trong thực tế, hướng dẫn sử dụng ARM ARMv7 nói

Kiến trúc này (ARMv7) được viết với kỳ vọng rằng tất cả các bộ xử lý sử dụng cùng một hệ điều hành hoặc trình ảo hóa đều nằm trong cùng một miền có thể chia sẻ có thể chia sẻ được

Vì vậy, bộ nhớ chia sẻ không kết hợp giữa các miền riêng biệt chỉ là một điều cho việc sử dụng cụ thể của các vùng bộ nhớ dùng chung để liên lạc giữa các tiến trình khác nhau dưới các nhân khác nhau.

Xem thêm cuộc thảo luận CoreCLR này về mã gen bằng cách sử dụng các rào cản bộ nhớ (Hệ thống có thể dmb ishchia sẻ bên trong) so với dmb sy(Hệ thống) trong trình biên dịch đó.

Tôi đưa ra khẳng định rằng không có triển khai C ++ nào cho bất kỳ ISA nào khác chạy std::threadtrên các lõi với bộ đệm không kết hợp. Tôi không có bằng chứng rằng không có triển khai như vậy tồn tại, nhưng có vẻ như rất khó xảy ra. Trừ khi bạn nhắm mục tiêu một mẩu CT kỳ lạ cụ thể hoạt động theo cách đó, suy nghĩ của bạn về hiệu suất sẽ giả định sự kết hợp bộ nhớ cache giống như MESI giữa tất cả các luồng. (Tuy nhiên, tốt nhất là sử dụng atomic<T>theo những cách đảm bảo tính chính xác!)


Bộ nhớ kết hợp làm cho nó đơn giản

Nhưng trên một hệ thống đa lõi với bộ nhớ kết hợp, việc triển khai một cửa hàng phát hành chỉ có nghĩa là đặt hàng cam kết vào bộ đệm cho các cửa hàng của luồng này, không thực hiện bất kỳ thao tác xóa rõ ràng nào. ( https://preshing.com/20120913/acquire-and-release-semantics/https://preshing.com/20120710/memory-barrier-are-like-source-control-operations/ ). (Và tải có nghĩa là yêu cầu truy cập vào bộ đệm trong lõi khác).

Một lệnh rào cản bộ nhớ chỉ chặn các tải và / hoặc lưu trữ của luồng hiện tại cho đến khi bộ đệm lưu trữ thoát ra; điều đó luôn luôn xảy ra nhanh nhất có thể. ( Có một rào cản bộ nhớ đảm bảo rằng sự kết hợp bộ nhớ cache đã được hoàn thành? Giải quyết quan niệm sai lầm này). Vì vậy, nếu bạn không cần đặt hàng, chỉ cần nhắc nhở trong các chủ đề khác, mo_relaxedlà ổn. (Và cũng vậy volatile, nhưng đừng làm vậy.)

Xem thêm ánh xạ C / C ++ 11 tới bộ xử lý

Sự thật thú vị: trên x86, mỗi cửa hàng asm là một cửa hàng phát hành vì mô hình bộ nhớ x86 về cơ bản là seq-cst cộng với bộ đệm lưu trữ (có chuyển tiếp cửa hàng).


Bán lại liên quan: lưu trữ bộ đệm, khả năng hiển thị toàn cầu và tính liên kết: C ++ 11 đảm bảo rất ít. Hầu hết các ISA thực (trừ PowerPC) đều đảm bảo rằng tất cả các luồng có thể đồng ý về thứ tự xuất hiện của hai cửa hàng bởi hai luồng khác. (Trong thuật ngữ mô hình bộ nhớ kiến ​​trúc máy tính chính thức, chúng là "nguyên tử đa bản sao").

Quan niệm sai lầm khác là hướng dẫn bộ nhớ hàng rào asm là cần thiết để tuôn ra bộ đệm cửa hàng cho lõi khác để xem các cửa hàng của chúng tôi ở tất cả . Trên thực tế, bộ đệm lưu trữ luôn cố gắng tự thoát (cam kết với bộ đệm L1d) càng nhanh càng tốt, nếu không nó sẽ lấp đầy và trì hoãn thực thi. Những gì một hàng rào / hàng rào đầy đủ làm là trì hoãn chuỗi hiện tại cho đến khi bộ đệm của cửa hàng bị cạn kiệt , do đó, các tải sau này của chúng tôi xuất hiện theo thứ tự toàn cầu sau các cửa hàng trước đó của chúng tôi.

(x86 ấy ra lệnh mạnh mẽ phương tiện mô hình bộ nhớ asm rằng volatiletrên x86 có thể sẽ đem lại cho bạn gần gũi hơn với mo_acq_rel, ngoại trừ việc thời gian biên dịch sắp xếp lại với các biến số phi nguyên tử vẫn có thể xảy ra. Nhưng hầu hết các phi x86 đã yếu theo lệnh mô hình bộ nhớ để volatilerelaxedkhoảng như yếu như mo_relaxedcho phép.)


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 .
Samuel Liew

2
Tuyệt vời viết lên. Đây chính xác là những gì tôi đang tìm kiếm (đưa ra tất cả sự thật) thay vì một tuyên bố về chăn chỉ nói "sử dụng nguyên tử thay vì dễ bay hơi cho một lá cờ boolean chung chung".
bernie

2
@bernie: Tôi đã viết điều này sau khi thất vọng bởi các tuyên bố lặp đi lặp lại rằng việc không sử dụng atomiccó thể dẫn đến các luồng khác nhau có các giá trị khác nhau cho cùng một biến trong bộ đệm . / facepalm. Trong bộ đệm, không, trong các thanh ghi CPU có (với các biến không nguyên tử); CPU sử dụng bộ đệm kết hợp. Tôi muốn các câu hỏi khác về SO không có nhiều lời giải thích cho atomicnhững quan niệm sai lầm đó về cách thức hoạt động của CPU. (Bởi vì đó là một điều hữu ích để hiểu vì lý do hiệu suất và cũng giúp giải thích lý do tại sao các quy tắc nguyên tử ISO C ++ được viết như hiện tại.)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Một khi một người phỏng vấn cũng tin rằng dễ bay hơi là vô ích với tôi rằng Tối ưu hóa sẽ không gây ra bất kỳ vấn đề nào và đang đề cập đến các lõi khác nhau có các dòng bộ đệm riêng biệt và tất cả những điều đó (không thực sự hiểu chính xác những gì anh ta đề cập đến). Nhưng đoạn mã này khi được biên dịch với -O3 trên g ++ (g ++ -O3 thread.cpp -lpthread), nó cho thấy hành vi không xác định. Về cơ bản nếu giá trị được đặt trước khi kiểm tra thì nó hoạt động tốt và nếu không, nó sẽ đi vào một vòng lặp mà không bận tâm đến việc lấy giá trị (mà thực sự đã được thay đổi bởi luồng khác). Về cơ bản tôi tin rằng giá trị của checkValue chỉ được tìm nạp một lần vào thanh ghi và không bao giờ được kiểm tra lại dưới mức tối ưu hóa cao nhất. Nếu nó được đặt thành true trước khi tìm nạp, nó hoạt động tốt và nếu không, nó sẽ đi vào một vòng lặp. Xin hãy sửa tôi nếu tôi sai.


4
Điều này có liên quan gì volatile? Vâng, mã này là UB - nhưng nó cũng là UB volatile.
David Schwartz

-2

Bạn cần dễ bay hơi và có thể khóa.

dễ bay hơi cho trình tối ưu hóa rằng giá trị có thể thay đổi không đồng bộ, do đó

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

sẽ đọc cờ mỗi lần xung quanh vòng lặp.

Nếu bạn tắt tối ưu hóa hoặc làm cho mọi biến số biến động, chương trình sẽ hoạt động giống nhau nhưng chậm hơn. dễ bay hơi chỉ có nghĩa là 'Tôi biết bạn có thể vừa đọc nó và biết nó nói gì, nhưng nếu tôi nói hãy đọc nó thì hãy đọc nó.

Khóa là một phần của chương trình. Vì vậy, nhân tiện, nếu bạn đang thực hiện semaphores thì trong số những thứ khác, chúng phải biến động. (Đừng thử nó, nó rất khó, có lẽ sẽ cần một trình biên dịch nhỏ hoặc các công cụ nguyên tử mới, và nó đã được thực hiện.)


1
Nhưng không phải điều này, và ví dụ tương tự trong phản hồi khác, chờ đợi bận rộn và do đó, điều gì đó nên tránh? Nếu đây là một ví dụ giả định, có bất kỳ ví dụ thực tế nào không bị tước đoạt?
David Preston

7
@Chris: Bận rộn chờ đợi đôi khi là một giải pháp tốt. Đặc biệt, nếu bạn mong đợi chỉ phải chờ một vài chu kỳ đồng hồ, nó mang ít chi phí hơn nhiều so với cách tiếp cận nặng hơn nhiều của việc đình chỉ luồng. Tất nhiên, như tôi đã đề cập trong các bình luận khác, các ví dụ như ví dụ này là thiếu sót vì họ cho rằng việc đọc / ghi vào cờ sẽ không được sắp xếp lại theo mã mà nó bảo vệ và không có sự đảm bảo nào được đưa ra, và vì vậy , volatilekhông thực sự hữu ích ngay cả trong trường hợp này. Nhưng bận rộn chờ đợi là một kỹ thuật đôi khi hữu ích.
jalf

3
@richard Có và không. Nửa đầu là chính xác. Nhưng điều này chỉ có nghĩa là CPU và trình biên dịch không được phép sắp xếp lại các biến dễ bay hơi đối với nhau. Nếu tôi đọc biến A dễ bay hơi và sau đó đọc biến B biến động, thì trình biên dịch phải phát ra mã được bảo đảm (ngay cả khi sắp xếp lại CPU) để đọc A trước B. Nhưng nó không đảm bảo về tất cả các truy cập biến không biến động . Chúng có thể được sắp xếp lại xung quanh việc đọc / ghi dễ bay hơi của bạn. Vì vậy, trừ khi bạn thực hiện tất cả các biến trong chương trình của bạn không ổn định, nó sẽ không cung cấp cho bạn đảm bảo bạn đang quan tâm
jalf

2
@ ctrl-alt-delor: Đó không phải volatilelà "không sắp xếp lại" nghĩa là gì. Bạn đang hy vọng điều đó có nghĩa là các cửa hàng sẽ trở nên hiển thị trên toàn cầu (đối với các luồng khác) theo thứ tự chương trình. Đó là những gì atomic<T>với memory_order_releasehoặc seq_cstmang lại cho bạn. Nhưng volatile chỉ cung cấp cho bạn một đảm bảo không sắp xếp lại thời gian biên dịch : mỗi truy cập sẽ xuất hiện trong mã asm theo thứ tự chương trình. Hữu ích cho một trình điều khiển thiết bị. Và hữu ích cho việc tương tác với trình xử lý ngắt, trình gỡ lỗi hoặc trình xử lý tín hiệu trên lõi / luồng hiện tại, nhưng không tương tác với các lõi khác.
Peter Cordes

1
volatiletrong thực tế là đủ để kiểm tra một keep_runningcờ như bạn đang làm ở đây: CPU thực luôn có bộ nhớ đệm kết hợp không yêu cầu xả thủ công. Nhưng không có lý do để giới thiệu volatilequa atomic<T>với mo_relaxed; bạn sẽ nhận được cùng một asm.
Peter Cordes
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.