Trong C ++ 11, thông thường không bao giờ sử dụng volatile
cho luồng, chỉ dành cho MMIO
Nhưng TL: DR, nó "hoạt động" giống như nguyên tử với mo_relaxed
phầ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ý. atomic
khô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_relaxed
khô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
, volatile
là 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::atomic
với mo_relaxed
lỗi thời volatile
cho 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::atomic
trên các trình biên dịch chính (như gcc và clang) không chỉ sử dụng volatile
nộ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 __atomic
builtins 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, volatile
có thể sử dụng trong thực tế cho những thứ như exit_now
cờ 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 volatile
hoạ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::atomic
với các lỗi thời của mo_relaxed volatile
cho luồng.
(Tiêu chuẩn ISO C ++ khá mơ hồ về nó, chỉ nói rằng các volatile
truy 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_now
lá 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_stuff
vì 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_now
là 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 để volatile
nó 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, volatile
không đảm bảo thiếu rách. Tôi đã bỏ qua sự khác biệt đó ở đây bool
vì 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 volatile
vẫ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_now
chờ đợi luồng khác thực sự thoát. Hoặc thậm chí là nó chờ cho exit_now=true
cử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_cst
sẽ 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> flag
vớ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, atomic
cũ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_cst
thứ 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 atomic
mã 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_relaxed
thay !! Đ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 volatile
truy 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 = flag
hoặ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 release
cử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í.
Có 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::thread
qua 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::thread
luồ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 ish
chia 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::thread
trê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/ và 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_relaxed
là ổ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 volatile
trê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ớ để volatile
và relaxed
khoảng như yếu như mo_relaxed
cho phép.)