Từ khóa biến động C ++ có giới thiệu hàng rào bộ nhớ không?


85

Tôi hiểu rằng điều đó volatilethông báo cho trình biên dịch rằng giá trị có thể bị thay đổi, nhưng để thực hiện chức năng này, trình biên dịch có cần giới thiệu hàng rào bộ nhớ để làm cho nó hoạt động không?

Theo hiểu biết của tôi, chuỗi hoạt động trên các đối tượng biến động không thể được sắp xếp lại và phải được giữ nguyên. Điều này dường như ngụ ý rằng một số hàng rào bộ nhớ là cần thiết và thực sự không có cách nào để giải quyết vấn đề này. Tôi nói điều này có đúng không?


Có một cuộc thảo luận thú vị tại câu hỏi liên quan này

Jonathan Wakely viết :

... Các quyền truy cập vào các biến biến động riêng biệt không thể được sắp xếp lại bởi trình biên dịch miễn là chúng xuất hiện trong các biểu thức đầy đủ riêng biệt ... đúng rằng biến biến động vô ích cho sự an toàn của luồng, nhưng không phải vì những lý do mà anh ta đưa ra. Đó không phải là vì trình biên dịch có thể sắp xếp lại thứ tự các quyền truy cập vào các đối tượng dễ bay hơi, mà vì CPU có thể sắp xếp lại chúng. Các hoạt động nguyên tử và rào cản bộ nhớ ngăn trình biên dịch và CPU sắp xếp lại thứ tự

David Schwartz trả lời trong các ý kiến :

... Không có sự khác biệt nào, theo quan điểm của tiêu chuẩn C ++, giữa trình biên dịch thực hiện điều gì đó và trình biên dịch phát ra các lệnh khiến phần cứng thực hiện điều gì đó. Nếu CPU có thể sắp xếp lại thứ tự các truy cập tới các chất bay hơi, thì tiêu chuẩn không yêu cầu phải giữ nguyên thứ tự của chúng. ...

... Tiêu chuẩn C ++ không phân biệt gì về việc sắp xếp lại thứ tự. Và bạn không thể tranh luận rằng CPU có thể sắp xếp lại thứ tự của chúng mà không có hiệu ứng quan sát được nên điều đó không sao - tiêu chuẩn C ++ định nghĩa thứ tự của chúng là có thể quan sát được. Một trình biên dịch tuân thủ tiêu chuẩn C ++ trên một nền tảng nếu nó tạo ra mã khiến nền tảng đó thực hiện những gì tiêu chuẩn yêu cầu. Nếu tiêu chuẩn yêu cầu quyền truy cập vào các chất bay hơi không được sắp xếp lại, thì một nền tảng sắp xếp lại chúng không tuân thủ. ...

Quan điểm của tôi là nếu tiêu chuẩn C ++ cấm trình biên dịch sắp xếp lại thứ tự các truy cập đến các chất bay hơi khác nhau, trên lý thuyết rằng thứ tự của các truy cập đó là một phần của hành vi có thể quan sát được của chương trình, thì nó cũng yêu cầu trình biên dịch phát ra mã cấm CPU thực hiện. vì thế. Tiêu chuẩn không phân biệt giữa những gì trình biên dịch làm và những gì mã tạo của trình biên dịch làm cho CPU làm.

Điều nào dẫn đến hai câu hỏi: Liệu một trong hai câu hỏi đó có "đúng" không? Triển khai thực tế thực sự làm gì?


9
Nó chủ yếu có nghĩa là trình biên dịch không nên giữ biến đó trong một thanh ghi. Mọi phép gán và đọc trong mã nguồn phải tương ứng với các truy cập bộ nhớ trong mã nhị phân.
Basile Starynkevitch


1
Tôi nghi ngờ vấn đề là bất kỳ hàng rào bộ nhớ nào sẽ không hiệu quả nếu giá trị được lưu trữ trong một thanh ghi nội bộ. Tôi nghĩ bạn vẫn cần thực hiện các biện pháp bảo vệ khác trong tình huống đồng thời.
Galik

Theo như tôi biết, biến được sử dụng cho các biến có thể được thay đổi bằng phần cứng (thường được sử dụng với vi điều khiển). Nó đơn giản có nghĩa là việc đọc biến không thể được thực hiện theo một thứ tự khác và không thể được tối ưu hóa. Đó là C mặc dù, nhưng phải giống nhau trong ++.
Mast

1
@Mast Tôi vẫn chưa thấy một trình biên dịch ngăn việc đọc các volatilebiến không được tối ưu hóa bởi bộ nhớ đệm CPU. Tất cả các trình biên dịch này đều không tuân thủ hoặc tiêu chuẩn không có nghĩa như bạn nghĩ. (Các tiêu chuẩn không phân biệt giữa những gì các trình biên dịch hiện và những gì các trình biên dịch làm cho CPU làm Đó là công việc của trình biên dịch sang mã Emit rằng, khi chạy, phù hợp với tiêu chuẩn..)
David Schwartz

Câu trả lời:


58

Thay vì giải thích những gì volatilecó, hãy cho phép tôi giải thích khi nào bạn nên sử dụng volatile.

  • Khi bên trong một bộ xử lý tín hiệu. Bởi vì việc ghi vào một volatilebiến là điều duy nhất mà tiêu chuẩn cho phép bạn làm từ bên trong bộ xử lý tín hiệu. Vì C ++ 11, bạn có thể sử dụng std::atomiccho mục đích đó, nhưng chỉ khi nguyên tử không bị khóa.
  • Khi xử lý setjmp theo Intel .
  • Khi xử lý trực tiếp với phần cứng và bạn muốn đảm bảo rằng trình biên dịch không tối ưu hóa việc đọc hoặc ghi của bạn.

Ví dụ:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

Nếu không có trình volatilexác định, trình biên dịch được phép tối ưu hóa hoàn toàn vòng lặp. Bộ volatilechỉ định cho trình biên dịch biết rằng nó có thể không giả định rằng 2 lần đọc tiếp theo trả về cùng một giá trị.

Lưu ý rằng volatilekhông liên quan gì đến chủ đề. Ví dụ trên không hoạt động nếu có một luồng khác được ghi vào *foovì không có hoạt động thu nhận nào liên quan.

Trong tất cả các trường hợp khác, việc sử dụng volatilephải được coi là không di động và không vượt qua xem xét mã nữa ngoại trừ khi xử lý các trình biên dịch trước C ++ 11 và tiện ích mở rộng trình biên dịch (chẳng hạn như /volatile:mscông tắc của msvc , được bật theo mặc định trong X86 / I64).


5
Nó nghiêm ngặt hơn "có thể không cho rằng 2 lần đọc tiếp theo trả về cùng một giá trị". Ngay cả khi bạn chỉ đọc một lần và / hoặc vứt bỏ (các) giá trị, việc đọc phải được thực hiện.
philipxy

1
Việc sử dụng trong bộ xử lý tín hiệu và setjmplà hai đảm bảo tiêu chuẩn tạo ra. Mặt khác, mục đích , ít nhất là lúc đầu, là để hỗ trợ IO được ánh xạ bộ nhớ. Mà trên một số bộ xử lý có thể yêu cầu một hàng rào hoặc một thanh ghi nhớ.
James Kanze

@philipxy Ngoại trừ không ai biết "đã đọc" nghĩa là gì. Ví dụ, không ai tin rằng việc đọc thực sự từ bộ nhớ phải được thực hiện - không có trình biên dịch nào mà tôi biết về việc cố gắng vượt qua bộ nhớ cache của CPU trên các volatiletruy cập.
David Schwartz

@JamesKanze: Không phải vậy. Các bộ xử lý tín hiệu lại tiêu chuẩn nói rằng trong quá trình xử lý tín hiệu, chỉ các đối tượng nguyên tử không có khóa std :: sig_atomic_t & lock-free mới có giá trị xác định. Nhưng nó cũng nói rằng việc tiếp cận các đối tượng dễ bay hơi là những tác dụng phụ có thể quan sát được.
philipxy

1
@DavidSchwartz Một số cặp kiến ​​trúc trình biên dịch ánh xạ chuỗi truy cập được chỉ định tiêu chuẩn đến các hiệu ứng thực tế và các chương trình làm việc truy cập các chất bay hơi để có được các hiệu ứng đó. Thực tế là một số cặp như vậy không có ánh xạ hoặc ánh xạ không hữu ích tầm thường có liên quan đến chất lượng của việc triển khai nhưng không đến mức cần thiết.
philipxy

25

Từ khóa biến động C ++ có giới thiệu hàng rào bộ nhớ không?

Một trình biên dịch C ++ phù hợp với đặc điểm kỹ thuật không bắt buộc phải giới thiệu hàng rào bộ nhớ. Trình biên dịch cụ thể của bạn có thể; chuyển câu hỏi của bạn đến các tác giả của trình biên dịch của bạn.

Chức năng "dễ bay hơi" trong C ++ không liên quan gì đến phân luồng. Hãy nhớ rằng, mục đích của "dễ bay hơi" là vô hiệu hóa tối ưu hóa trình biên dịch để việc đọc từ một thanh ghi đang thay đổi do các điều kiện ngoại sinh không được tối ưu hóa. Địa chỉ bộ nhớ đang được ghi bởi một luồng khác trên một CPU khác có phải là thanh ghi đang thay đổi do các điều kiện ngoại sinh không? Một lần nữa, nếu một số tác giả trình biên dịch đã chọn xử lý các địa chỉ bộ nhớ được ghi bởi các luồng khác nhau trên các CPU khác nhau như thể chúng là các thanh ghi thay đổi do các điều kiện ngoại sinh, thì đó là việc của họ; họ không bắt buộc phải làm như vậy. Họ cũng không cần - thậm chí nếu nó giới thiệu một hàng rào bộ nhớ - để, ví dụ, đảm bảo rằng tất cả các chủ đề thấy một phù hợp thứ tự đọc và ghi biến động.

Trên thực tế, dễ bay hơi vô dụng đối với việc phân luồng trong C / C ++. Cách tốt nhất là tránh nó.

Hơn nữa: hàng rào bộ nhớ là một chi tiết triển khai của các kiến ​​trúc bộ xử lý cụ thể. Trong C #, nơi dễ thay đổi được thiết kế cho đa luồng, đặc điểm kỹ thuật không nói rằng một nửa hàng rào sẽ được giới thiệu, vì chương trình có thể đang chạy trên một kiến ​​trúc không có hàng rào ngay từ đầu. Thay vào đó, một lần nữa, đặc tả đảm bảo một số đảm bảo (cực kỳ yếu) về những gì tối ưu hóa sẽ được trình biên dịch, thời gian chạy và CPU tránh để đưa ra các ràng buộc nhất định (cực kỳ yếu) về cách một số tác dụng phụ sẽ được sắp xếp. Trong thực tế, những tối ưu hóa này bị loại bỏ bằng cách sử dụng một nửa hàng rào, nhưng đó là chi tiết triển khai có thể thay đổi trong tương lai.

Việc bạn quan tâm đến ngữ nghĩa của biến đổi trong bất kỳ ngôn ngữ nào vì chúng liên quan đến đa luồng cho thấy rằng bạn đang nghĩ đến việc chia sẻ bộ nhớ trên các luồng. Hãy xem xét đơn giản là không làm điều đó. Nó làm cho chương trình của bạn khó hiểu hơn nhiều và có nhiều khả năng chứa các lỗi tinh vi, không thể tái tạo.


19
"dễ bay hơi là vô dụng trong C / C ++." Không có gì! Bạn có một cái nhìn rất tập trung vào chế độ máy tính để bàn về thế giới ... nhưng hầu hết mã C và C ++ chạy trên các hệ thống nhúng, nơi rất cần thiết cho I / O được ánh xạ bộ nhớ.
Ben Voigt

12
Và lý do mà quyền truy cập biến động được bảo toàn không chỉ đơn giản là vì các điều kiện ngoại sinh có thể thay đổi vị trí bộ nhớ. Chính quyền truy cập có thể kích hoạt các hành động khác. Ví dụ, việc đọc để tăng FIFO hoặc xóa cờ ngắt là rất phổ biến.
Ben Voigt

3
@BenVoigt: Ý nghĩa của tôi là vô dụng để đối phó hiệu quả với những tai ương về luồng.
Eric Lippert

4
@DavidSchwartz Tiêu chuẩn rõ ràng không thể đảm bảo cách hoạt động của IO được ánh xạ bộ nhớ. Nhưng IO được ánh xạ bộ nhớ là lý do tại sao lại volatileđược đưa vào tiêu chuẩn C. Tuy nhiên, vì tiêu chuẩn không thể chỉ định những thứ như những gì thực sự xảy ra tại một "quyền truy cập", nó nói rằng "Điều gì tạo nên quyền truy cập vào một đối tượng có loại đủ điều kiện dễ bay hơi được xác định bằng cách triển khai." Quá nhiều triển khai ngày nay không cung cấp một định nghĩa hữu ích về một quyền truy cập, mà IMHO vi phạm tinh thần của tiêu chuẩn, ngay cả khi nó phù hợp với chữ cái.
James Kanze

8
Chỉnh sửa đó là một cải tiến rõ ràng, nhưng lời giải thích của bạn vẫn quá tập trung vào "bộ nhớ có thể bị thay đổi ngoại sinh". volatilengữ nghĩa mạnh hơn thế, trình biên dịch phải tạo ra mọi truy cập được yêu cầu (1.9 / 8, 1.9 / 12), không chỉ đơn giản đảm bảo rằng các thay đổi ngoại sinh cuối cùng được phát hiện (1.10 / 27). Trong thế giới của I / O được ánh xạ bộ nhớ, một lần đọc bộ nhớ có thể có logic liên kết tùy ý, giống như một bộ lấy thuộc tính. Bạn sẽ không tối ưu hóa các cuộc gọi đến người nhận thuộc tính theo các quy tắc mà bạn đã nêu volatile, Tiêu chuẩn cũng không cho phép điều đó.
Ben Voigt

13

Những gì David đang quan tâm là thực tế rằng tiêu chuẩn C ++ chỉ định hành vi của một số luồng chỉ tương tác trong các tình huống cụ thể và mọi thứ khác dẫn đến hành vi không xác định. Điều kiện chủng tộc liên quan đến ít nhất một lần ghi là không xác định nếu bạn không sử dụng các biến nguyên tử.

Do đó, trình biên dịch hoàn toàn có quyền bỏ qua bất kỳ hướng dẫn đồng bộ hóa nào vì CPU của bạn sẽ chỉ nhận thấy sự khác biệt trong một chương trình thể hiện hành vi không xác định do thiếu đồng bộ hóa.


5
Giải thích độc đáo, cảm ơn bạn. Tiêu chuẩn chỉ xác định trình tự truy cập đến các chất bay hơi có thể quan sát được miễn là chương trình không có hành vi không xác định .
Jonathan Wakely

4
Nếu chương trình có một cuộc đua dữ liệu thì tiêu chuẩn không đưa ra yêu cầu nào về hành vi quan sát được của chương trình. Trình biên dịch sẽ không thêm các rào cản đối với các truy cập dễ bay hơi để ngăn chặn các cuộc chạy đua dữ liệu có trong chương trình, đó là công việc của lập trình viên, bằng cách sử dụng các rào cản rõ ràng hoặc các phép toán nguyên tử.
Jonathan Wakely

Bạn nghĩ tại sao tôi bỏ qua điều đó? Bạn nghĩ phần nào trong lập luận của tôi không hợp lệ? Tôi 100% đồng ý rằng trình biên dịch hoàn toàn có quyền từ bỏ bất kỳ đồng bộ hóa nào.
David Schwartz

2
Điều này chỉ đơn giản là sai, hoặc ít nhất, nó bỏ qua điều cần thiết. volatilekhông liên quan gì đến chủ đề; mục đích ban đầu là hỗ trợ IO được ánh xạ bộ nhớ. Và ít nhất trên một số bộ xử lý, hỗ trợ IO được ánh xạ bộ nhớ sẽ yêu cầu hàng rào. (Trình biên dịch không làm điều này, nhưng đó là một vấn đề khác nhau.)
James Kanze

@JamesKanze liên quan volatilenhiều đến các luồng: volatilexử lý bộ nhớ có thể được truy cập mà không cần trình biên dịch biết rằng nó có thể được truy cập và bao gồm nhiều cách sử dụng dữ liệu được chia sẻ trong thế giới thực giữa các luồng trên CPU cụ thể.
tò mò

12

Trước hết, các tiêu chuẩn C ++ không đảm bảo các rào cản bộ nhớ cần thiết để sắp xếp đúng thứ tự đọc / ghi không phải là nguyên tử. Các biến biến động được khuyến nghị sử dụng với MMIO, xử lý tín hiệu, v.v. Trên hầu hết các triển khai, biến biến động không hữu ích cho đa luồng và thường không được khuyến khích.

Về việc thực hiện các truy cập biến động, đây là lựa chọn của trình biên dịch.

Bài viết này , mô tả hành vi gcc cho thấy rằng bạn không thể sử dụng một đối tượng dễ bay hơi làm rào cản bộ nhớ để sắp xếp một chuỗi các lần ghi vào bộ nhớ dễ bay hơi.

Về hành vi của icc, tôi thấy nguồn này cũng nói rằng dễ bay hơi không đảm bảo sắp xếp các truy cập bộ nhớ.

Trình biên dịch Microsoft VS2013 có một hành vi khác. Tài liệu này giải thích cách dễ bay hơi thực thi ngữ nghĩa Phát hành / Có được và cho phép các đối tượng dễ bay hơi được sử dụng trong khóa / bản phát hành trên các ứng dụng đa luồng.

Một khía cạnh khác cần được xem xét là cùng một trình biên dịch có thể có một wrt hành vi khác nhau. dễ bay hơi tùy thuộc vào kiến ​​trúc phần cứng được nhắm mục tiêu . Bài đăng này liên quan đến trình biên dịch MSVS 2013 nêu rõ các chi tiết cụ thể của việc biên dịch với các nền tảng ARM.

Vì vậy, câu trả lời của tôi cho:

Từ khóa biến động C ++ có giới thiệu hàng rào bộ nhớ không?

sẽ là: Không được đảm bảo, có thể là không nhưng một số trình biên dịch có thể làm điều đó. Bạn không nên dựa vào thực tế là nó có.


2
Nó không ngăn cản tối ưu hóa, nó chỉ ngăn trình biên dịch thay đổi tải và lưu trữ ngoài những ràng buộc nhất định.
Dietrich Epp

Không rõ bạn đang nói gì. Bạn có nói rằng nó xảy ra trường hợp trên một số trình biên dịch không xác định volatilengăn trình biên dịch sắp xếp lại các tải / lưu trữ không? Hay bạn đang nói tiêu chuẩn C ++ yêu cầu nó phải làm như vậy? Và nếu sau này, bạn có thể trả lời lập luận của tôi ngược lại được trích dẫn trong câu hỏi ban đầu không?
David Schwartz

@DavidSchwartz Tiêu chuẩn này ngăn chặn việc sắp xếp lại thứ tự (từ bất kỳ nguồn nào) các truy cập thông qua một volatilelvalue. Tuy nhiên, vì nó để lại định nghĩa về "quyền truy cập" cho việc triển khai, điều này không mang lại nhiều lợi ích cho chúng ta nếu việc triển khai không quan tâm.
James Kanze

Tôi nghĩ rằng một số phiên bản của trình biên dịch MSC đã thực hiện ngữ nghĩa hàng rào cho volatile, nhưng không có hàng rào ở các mã được tạo từ trình biên dịch trong Visual Studio 2012.
James Kanze

@JamesKanze Về cơ bản có nghĩa là hành vi di động duy nhất của volatilenó được liệt kê cụ thể theo tiêu chuẩn. ( setjmp, tín hiệu, v.v.)
David Schwartz,

7

Trình biên dịch chỉ chèn một hàng rào bộ nhớ trên kiến ​​trúc Itanium, theo như tôi biết.

Các volatiletừ khóa được thực sự tốt nhất được sử dụng để thay đổi không đồng bộ, ví dụ, xử lý tín hiệu và ghi nhớ ánh xạ; nó thường là công cụ sai khi sử dụng cho lập trình đa luồng.


1
Sắp xếp. 'trình biên dịch' (msvc) chèn hàng rào bộ nhớ khi một kiến ​​trúc không phải là ARM được nhắm mục tiêu và chuyển đổi / variable: ms được sử dụng (mặc định). Xem msdn.microsoft.com/en-us/library/12a04hfd.aspx . Các trình biên dịch khác không chèn hàng rào vào các biến biến động theo hiểu biết của tôi. Nên tránh sử dụng biến động trừ khi xử lý trực tiếp với phần cứng, trình xử lý tín hiệu hoặc trình biên dịch không tuân theo c ++ 11.
Stefan

@Stefan No. volatilecực kỳ hữu ích cho nhiều mục đích sử dụng không liên quan đến phần cứng. Bất cứ khi nào bạn muốn việc triển khai tạo mã CPU tuân theo mã C / C ++ chặt chẽ, hãy sử dụng volatile.
tò mòguy

7

Nó phụ thuộc vào trình biên dịch "trình biên dịch" là. Visual C ++ có, kể từ năm 2005. Nhưng Tiêu chuẩn không yêu cầu nó, vì vậy một số trình biên dịch khác thì không.


VC ++ 2012 dường như không để chèn một hàng rào: int volatile i; int main() { return i; }tạo ra một chính với chính xác hai hướng dẫn: mov eax, i; ret 0;.
James Kanze

@JamesKanze: Chính xác là phiên bản nào? Và bạn có đang sử dụng bất kỳ tùy chọn biên dịch không mặc định nào không? Tôi đang dựa vào tài liệu (phiên bản bị ảnh hưởng đầu tiên)(phiên bản mới nhất) , trong đó chắc chắn đề cập đến ngữ nghĩa thu được và phát hành.
Ben Voigt

cl /helpcho biết phiên bản 18.00.21005.1. Thư mục của nó là C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC. Tiêu đề trên cửa sổ lệnh cho biết VS 2013. Vì vậy, liên quan đến phiên bản ... Các tùy chọn duy nhất tôi đã sử dụng là /c /O2 /Fa. (Nếu không có /O2, nó cũng thiết lập các khung stack địa phương Nhưng vẫn không có hướng dẫn hàng rào..)
James Kanze

@JamesKanze: Tôi quan tâm hơn đến kiến ​​trúc, ví dụ: "Microsoft (R) C / C ++ Tối ưu hóa phiên bản trình biên dịch 18.00.30723 cho x64" Có lẽ không có hàng rào nào vì x86 và x64 có đảm bảo đồng tiền bộ nhớ đệm khá mạnh trong mô hình bộ nhớ của họ. ?
Ben Voigt

Có lẽ. Tôi thực sự không biết. Thực tế là tôi đã làm điều này main, vì vậy trình biên dịch có thể xem toàn bộ chương trình và biết rằng không có chuỗi nào khác hoặc ít nhất là không có quyền truy cập nào khác vào biến trước của tôi (vì vậy không thể có vấn đề về bộ nhớ cache) có thể hình dung ảnh hưởng đến điều này tốt, nhưng bằng cách nào đó, tôi nghi ngờ điều đó.
James Kanze

5

Phần lớn là từ bộ nhớ và dựa trên trước C ++ 11, không có luồng. Nhưng đã tham gia vào các cuộc thảo luận về phân luồng trong cam kết, tôi có thể nói rằng ủy ban không bao giờ có ý địnhvolatile có thể được sử dụng để đồng bộ hóa giữa các luồng. Microsoft đã đề xuất nó, nhưng đề xuất này không thực hiện được.

Đặc điểm kỹ thuật quan trọng volatilelà quyền truy cập vào một biến thể đại diện cho một "hành vi có thể quan sát được", giống như IO. Theo cách tương tự, trình biên dịch không thể sắp xếp lại hoặc loại bỏ IO cụ thể, nó không thể sắp xếp lại hoặc xóa các quyền truy cập vào một đối tượng dễ bay hơi (hay nói đúng hơn, truy cập thông qua một biểu thức giá trị với kiểu đủ điều kiện dễ bay hơi). Trên thực tế, mục đích ban đầu của portable là hỗ trợ IO được ánh xạ bộ nhớ. Tuy nhiên, "vấn đề" với điều này là việc triển khai được xác định những gì tạo thành một "truy cập dễ bay hơi". Và nhiều trình biên dịch thực hiện nó như thể định nghĩa là "một lệnh đọc hoặc ghi vào bộ nhớ đã được thực thi". Đó là một định nghĩa hợp pháp, mặc dù vô dụng, nếu việc triển khai chỉ rõ nó. (Tôi vẫn chưa tìm thấy thông số kỹ thuật thực sự cho bất kỳ trình biên dịch nào.

Có thể cho rằng (và đó là lập luận mà tôi chấp nhận), điều này vi phạm mục đích của tiêu chuẩn, vì trừ khi phần cứng nhận ra các địa chỉ là IO được ánh xạ bộ nhớ và ngăn chặn bất kỳ sắp xếp lại nào, v.v., bạn thậm chí không thể sử dụng dễ bay hơi cho IO được ánh xạ bộ nhớ, ít nhất là trên kiến ​​trúc Sparc hoặc Intel. Không bao giờ kém hơn, không có trình soạn thảo nào mà tôi đã xem (Sun CC, g ++ và MSC) xuất ra bất kỳ hướng dẫn hàng rào hoặc thanh ghi nhớ nào. (Khoảng thời gian Microsoft đề xuất mở rộng các quy tắc volatile, tôi nghĩ rằng một số trình biên dịch của họ đã thực hiện đề xuất của họ và đã đưa ra các hướng dẫn hàng rào cho các truy cập dễ bay hơi. Tôi chưa xác minh những trình biên dịch gần đây làm gì, nhưng tôi sẽ không ngạc nhiên nếu nó phụ thuộc trên một số tùy chọn trình biên dịch. Phiên bản tôi đã kiểm tra - tuy nhiên, tôi nghĩ đó là VS6.0 - không phát ra hàng rào.)


Tại sao bạn chỉ nói rằng trình biên dịch không thể sắp xếp lại hoặc xóa quyền truy cập vào các đối tượng dễ bay hơi? Chắc chắn nếu các truy cập là hành vi có thể quan sát được, thì chắc chắn điều quan trọng không kém là ngăn không cho CPU, ghi bộ đệm đăng, bộ điều khiển bộ nhớ và mọi thứ khác sắp xếp lại chúng.
David Schwartz,

@DavidSchwartz Bởi vì đó là những gì tiêu chuẩn nói. Chắc chắn, từ quan điểm thực tế, những gì mà các trình biên dịch mà tôi đã xác minh làm là hoàn toàn vô dụng, nhưng những từ ngữ tiêu chuẩn này đủ để họ vẫn có thể yêu cầu sự phù hợp (hoặc có thể, nếu họ thực sự ghi lại nó).
James Kanze

1
@DavidSchwartz: Đối với I / O được ánh xạ bộ nhớ độc quyền (hoặc mutex'd) tới các thiết bị ngoại vi, volatilengữ nghĩa là hoàn toàn phù hợp. Nói chung, các thiết bị ngoại vi như vậy báo cáo vùng bộ nhớ của chúng là không thể lưu vào bộ nhớ cache, điều này giúp sắp xếp lại thứ tự ở cấp phần cứng.
Ben Voigt

@BenVoigt Tôi tự hỏi bằng cách nào đó về điều đó: ý tưởng rằng bộ xử lý bằng cách nào đó "biết" rằng địa chỉ mà nó đang xử lý là IO được ánh xạ bộ nhớ. Theo như tôi biết, Sparcs không có bất kỳ hỗ trợ nào cho việc này, vì vậy điều đó sẽ vẫn khiến Sun CC và g ++ trên Sparc không thể sử dụng được cho IO được ánh xạ bộ nhớ. (Khi tôi xem xét vấn đề này, tôi chủ yếu quan tâm đến Sparc.)
James Kanze

@JamesKanze: Theo những gì tôi đã tìm kiếm nhỏ, có vẻ như Sparc có các dải địa chỉ dành riêng cho "các chế độ xem thay thế" của bộ nhớ không thể lưu vào bộ nhớ cache. Miễn là các điểm truy cập dễ thay đổi của bạn vàoASI_REAL_IO phần không gian địa chỉ, tôi nghĩ bạn sẽ ổn. (Altera NIOS sử dụng một kỹ thuật tương tự, với các bit cao của địa chỉ điều khiển bỏ qua MMU; tôi chắc chắn rằng có những người khác cũng vậy)
Ben Voigt

5

Nó không cần phải làm thế. Dễ bay hơi không phải là một nguyên thủy đồng bộ hóa. Nó chỉ vô hiệu hóa tính năng tối ưu, tức là bạn nhận được một chuỗi đọc và ghi có thể đoán trước được trong một luồng theo thứ tự như được quy định bởi máy trừu tượng. Nhưng đọc và viết trong các chủ đề khác nhau không có thứ tự ngay từ đầu, nó không có ý nghĩa gì khi nói về việc giữ gìn hoặc không giữ gìn trật tự của chúng. Thứ tự giữa các nút có thể được thiết lập bằng cách đồng bộ hóa ban đầu, bạn sẽ có UB mà không cần chúng.

Một chút giải thích liên quan đến rào cản bộ nhớ. Một CPU điển hình có một số cấp độ truy cập bộ nhớ. Có một đường dẫn bộ nhớ, một số cấp bộ nhớ cache, sau đó là RAM, v.v.

Hướng dẫn trên thanh ghi nhớ làm sạch đường ống. Chúng không thay đổi thứ tự thực hiện đọc và ghi, nó chỉ buộc những thứ chưa thực hiện được tại một thời điểm nhất định. Nó hữu ích cho các chương trình đa luồng, nhưng không nhiều.

(Các) bộ nhớ đệm thường tự động kết hợp giữa các CPU. Nếu muốn đảm bảo bộ nhớ đệm đồng bộ với RAM, thì cần xóa bộ nhớ đệm. Nó rất khác với một thanh ghi nhớ.


1
Vì vậy, bạn đang nói rằng tiêu chuẩn C ++ nói rằng volatilechỉ tắt tối ưu hóa trình biên dịch? Điều đó không có ý nghĩa gì. Mọi tối ưu hóa mà trình biên dịch có thể thực hiện, ít nhất về nguyên tắc, CPU cũng có thể thực hiện tốt như nhau. Vì vậy, nếu tiêu chuẩn nói rằng nó chỉ vô hiệu hóa tối ưu hóa trình biên dịch, điều đó có nghĩa là nó sẽ không cung cấp hành vi nào mà người ta có thể dựa vào trong mã di động. Nhưng điều đó rõ ràng là không đúng vì mã di động có thể dựa vào hành vi của nó đối với setjmpvà các tín hiệu.
David Schwartz,

1
@DavidSchwartz Không, tiêu chuẩn không nói như vậy. Vô hiệu hóa tối ưu hóa chỉ là những gì thường được thực hiện để triển khai tiêu chuẩn. Tiêu chuẩn yêu cầu rằng hành vi có thể quan sát được xảy ra theo thứ tự như yêu cầu của máy trừu tượng. Khi máy trừu tượng không yêu cầu bất kỳ thứ tự nào, việc thực hiện được tự do sử dụng bất kỳ thứ tự nào hoặc không có thứ tự nào cả. Quyền truy cập vào các biến biến động trong các luồng khác nhau không được sắp xếp trừ khi áp dụng đồng bộ hóa bổ sung.
n. 'đại từ' m.

1
@DavidSchwartz Tôi xin lỗi vì đã diễn đạt không chính xác. Tiêu chuẩn này không yêu cầu các tính năng tối ưu bị vô hiệu hóa. Nó không có khái niệm về tối ưu hóa. Thay vào đó, nó chỉ định hành vi mà trong thực tế yêu cầu trình biên dịch vô hiệu hóa một số tính năng tối ưu nhất định theo cách mà trình tự đọc và ghi có thể quan sát được tuân thủ tiêu chuẩn.
n. 'đại từ' m.

1
Ngoại trừ nó không yêu cầu điều đó, bởi vì tiêu chuẩn cho phép triển khai xác định "chuỗi đọc và ghi có thể quan sát được" theo cách họ muốn. Nếu các triển khai chọn xác định các trình tự có thể quan sát được để tối ưu hóa phải bị vô hiệu hóa, thì chúng sẽ thực hiện. Nếu không, thì không. Bạn nhận được một chuỗi đọc và ghi có thể dự đoán được nếu và chỉ khi, việc triển khai đã chọn cung cấp cho bạn.
David Schwartz

1
Không, việc triển khai cần xác định những gì tạo thành một truy cập duy nhất. Trình tự của các truy cập như vậy được quy định bởi máy trừu tượng. Một quá trình thực hiện phải duy trì thứ tự. Tiêu chuẩn nói rõ ràng rằng "dễ bay hơi là một gợi ý cho việc triển khai để tránh tối ưu hóa tích cực liên quan đến đối tượng", mặc dù trong một phần không quy chuẩn, nhưng mục đích là rõ ràng.
n. 'đại từ' m.

4

Trình biên dịch cần giới thiệu một hàng rào bộ nhớ xung quanh các volatiletruy cập nếu và chỉ khi, điều đó là cần thiết để sử dụng cho volatilecông việc chuẩn ( setjmp, trình xử lý tín hiệu, v.v.) trên nền tảng cụ thể đó.

Lưu ý rằng một số trình biên dịch làm đi xa hơn những gì yêu cầu của tiêu chuẩn C ++ để làm cho volatilemạnh mẽ hơn hoặc hữu ích hơn trên các nền tảng đó. Mã di động không nên dựa vào volatileđể làm bất cứ điều gì ngoài những gì được chỉ định trong tiêu chuẩn C ++.


2

Tôi luôn sử dụng dễ bay hơi trong các quy trình dịch vụ ngắt, ví dụ như ISR (thường là mã hợp ngữ) sửa đổi một số vị trí bộ nhớ và mã cấp cao hơn chạy bên ngoài ngữ cảnh ngắt truy cập vị trí bộ nhớ thông qua một con trỏ tới biến động.

Tôi làm điều này cho RAM cũng như IO được ánh xạ bộ nhớ.

Dựa trên các cuộc thảo luận ở đây, có vẻ như đây vẫn là một cách sử dụng hợp lệ của dễ bay hơi nhưng không liên quan gì đến nhiều luồng hoặc CPU. Nếu trình biên dịch cho một bộ vi điều khiển "biết" rằng không thể có bất kỳ quyền truy cập nào khác (ví dụ: mọi thứ đều trên chip, không có bộ nhớ cache và chỉ có một lõi), tôi sẽ nghĩ rằng hàng rào bộ nhớ hoàn toàn không được ngụ ý, trình biên dịch chỉ cần ngăn chặn những tối ưu nhất định.

Khi chúng tôi chất thêm nhiều thứ vào "hệ thống" thực thi mã đối tượng, hầu như tất cả các cược đều tắt, ít nhất đó là cách tôi đọc cuộc thảo luận này. Làm thế nào một trình biên dịch có thể bao gồm tất cả các cơ sở?


0

Tôi nghĩ rằng sự nhầm lẫn xung quanh việc sắp xếp lại thứ tự lệnh và biến động bắt nguồn từ 2 khái niệm sắp xếp lại thứ tự CPU làm:

  1. Thực hiện không theo thứ tự.
  2. Trình tự đọc / ghi bộ nhớ như được thấy bởi các CPU khác (sắp xếp lại theo nghĩa là mỗi CPU có thể thấy một trình tự khác nhau).

Tính dễ bay hơi ảnh hưởng đến cách trình biên dịch tạo mã giả sử thực thi một luồng (điều này bao gồm cả các ngắt). Nó không ám chỉ bất cứ điều gì về các hướng dẫn về rào cản bộ nhớ, nhưng nó ngăn cản trình biên dịch thực hiện một số loại tối ưu hóa liên quan đến truy cập bộ nhớ.
Ví dụ điển hình là tìm nạp lại một giá trị từ bộ nhớ, thay vì sử dụng một giá trị được lưu trong bộ nhớ đệm trong một thanh ghi.

Thực hiện không theo thứ tự

CPU có thể thực thi các lệnh không theo thứ tự / suy đoán với điều kiện là kết quả cuối cùng có thể xảy ra trong mã gốc. CPU có thể thực hiện các phép biến đổi không được phép trong trình biên dịch vì trình biên dịch chỉ có thể thực hiện các phép biến đổi đúng trong mọi trường hợp. Ngược lại, CPU có thể kiểm tra tính hợp lệ của những tối ưu hóa này và loại bỏ chúng nếu chúng không chính xác.

Trình tự đọc / ghi bộ nhớ như được thấy bởi các CPU khác

Kết quả cuối cùng của một chuỗi lệnh, thứ tự có hiệu lực, phải phù hợp với ngữ nghĩa của mã do trình biên dịch tạo ra. Tuy nhiên, thứ tự thực thi thực tế do CPU chọn có thể khác. Thứ tự hiệu quả như được thấy trong các CPU khác (mỗi CPU có thể có một chế độ xem khác nhau) có thể bị hạn chế bởi các rào cản bộ nhớ.
Tôi không chắc thứ tự hiệu quả và thực tế có thể khác nhau ở mức độ nào vì tôi không biết rào cản bộ nhớ có thể ngăn cản CPU thực thi không theo thứ tự ở mức độ nào.

Nguồn:


0

Trong khi tôi đang làm việc thông qua một video hướng dẫn có thể tải xuống trực tuyến để phát triển 3D Graphics & Game Engine làm việc với OpenGL hiện đại. Chúng tôi đã sử dụng volatiletrong một trong các lớp học của mình. Trang web hướng dẫn có thể được tìm thấy ở đây và video làm việc với volatiletừ khóa được tìm thấy trong Shader Enginevideo loạt bài 98. Những tác phẩm này không phải của riêng tôi nhưng đã được công nhận Marek A. Krzeminski, MAScvà đây là một đoạn trích từ trang tải xuống video.

"Vì bây giờ chúng ta có thể để các trò chơi của mình chạy trong nhiều luồng nên điều quan trọng là phải đồng bộ hóa dữ liệu giữa các luồng đúng cách. Trong video này, tôi hướng dẫn cách tạo lớp khóa volitile để đảm bảo các biến volitile được đồng bộ hóa đúng cách ..."

Và nếu bạn đã đăng ký trang web của anh ấy và có quyền truy cập vào video của anh ấy trong video này, anh ấy sẽ tham khảo bài viết này liên quan đến việc sử dụng Volatilevới multithreadinglập trình.

Đây là bài viết từ liên kết trên: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

dễ bay hơi: Người bạn tốt nhất của lập trình viên đa luồng

Bởi Andrei Alexandrescu, ngày 01 tháng 2 năm 2001

Từ khóa dễ bay hơi được tạo ra để ngăn chặn các tối ưu hóa trình biên dịch có thể làm cho mã không chính xác khi có các sự kiện không đồng bộ nhất định.

Tôi không muốn làm hỏng tâm trạng của bạn, nhưng cột này đề cập đến chủ đề đáng sợ của lập trình đa luồng. Nếu - như phần trước của Generic đã nói - lập trình an toàn ngoại lệ là khó, thì đó là trò chơi của trẻ con so với lập trình đa luồng.

Nói chung, các chương trình sử dụng nhiều luồng là khó viết, chứng minh là đúng, gỡ lỗi, duy trì và chế ngự. Các chương trình đa luồng không chính xác có thể chạy trong nhiều năm mà không gặp trục trặc, chỉ để chạy bất ngờ vì một số điều kiện thời gian quan trọng đã được đáp ứng.

Không cần phải nói, một lập trình viên viết mã đa luồng cần tất cả sự trợ giúp mà cô ấy có thể nhận được. Cột này tập trung vào các điều kiện chủng tộc - một nguồn rắc rối phổ biến trong các chương trình đa luồng - và cung cấp cho bạn thông tin chi tiết và công cụ về cách tránh chúng và đáng kinh ngạc là trình biên dịch đã làm việc chăm chỉ để giúp bạn với điều đó.

Chỉ là một từ khóa nhỏ

Mặc dù cả hai Tiêu chuẩn C và C ++ đều im lặng rõ ràng khi nói đến các luồng, nhưng chúng thực sự nhượng bộ một chút đối với đa luồng, dưới dạng từ khóa biến động.

Cũng giống như const đối trọng nổi tiếng hơn của nó, dễ bay hơi là một công cụ sửa đổi kiểu. Nó nhằm mục đích được sử dụng cùng với các biến được truy cập và sửa đổi trong các luồng khác nhau. Về cơ bản, nếu không có tính dễ bay hơi, việc viết các chương trình đa luồng sẽ trở nên bất khả thi, hoặc trình biên dịch sẽ lãng phí các cơ hội tối ưu hóa rộng lớn. Một lời giải thích là theo thứ tự.

Hãy xem xét đoạn mã sau:

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

Mục đích của Gadget :: Wait ở trên là để kiểm tra biến flag_ member mỗi giây và trả về khi biến đó đã được một luồng khác đặt thành true. Ít nhất đó là những gì lập trình viên của nó dự định, nhưng, than ôi, Chờ là không chính xác.

Giả sử trình biên dịch chỉ ra rằng Sleep (1000) là một lệnh gọi vào thư viện bên ngoài mà không thể sửa đổi biến thành viên flag_. Sau đó, trình biên dịch kết luận rằng nó có thể cache flag_ trong một thanh ghi và sử dụng thanh ghi đó thay vì truy cập vào bộ nhớ trên bo mạch chậm hơn. Đây là một sự tối ưu hóa tuyệt vời cho mã một luồng, nhưng trong trường hợp này, nó gây hại cho tính đúng đắn: sau khi bạn gọi Wait đối với một số đối tượng Tiện ích, mặc dù một luồng khác gọi Wakeup, Wait sẽ lặp lại mãi mãi. Điều này là do sự thay đổi của flag_ sẽ không được phản ánh trong thanh ghi lưu trữ flag_. Tối ưu hóa quá ... lạc quan.

Bộ nhớ đệm các biến trong sổ đăng ký là một cách tối ưu hóa rất có giá trị được áp dụng hầu hết thời gian, vì vậy sẽ rất tiếc nếu lãng phí nó. C và C ++ cho bạn cơ hội vô hiệu hóa bộ nhớ đệm như vậy một cách rõ ràng. Nếu bạn sử dụng công cụ sửa đổi dễ thay đổi trên một biến, trình biên dịch sẽ không lưu biến đó vào bộ nhớ cache - mỗi lần truy cập sẽ truy cập vào vị trí bộ nhớ thực của biến đó. Vì vậy, tất cả những gì bạn phải làm để kết hợp Chờ / Đánh thức của Tiện ích hoạt động là đủ điều kiện flag_ một cách thích hợp:

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

Hầu hết các giải thích về cơ sở lý luận và cách sử dụng biến đổi đều dừng ở đây và khuyên bạn nên đủ điều kiện biến động các loại nguyên thủy mà bạn sử dụng trong nhiều chuỗi. Tuy nhiên, bạn có thể làm được nhiều điều hơn nữa với dễ bay hơi, vì nó là một phần của hệ thống kiểu tuyệt vời của C ++.

Sử dụng dễ bay hơi với các loại do người dùng xác định

Bạn có thể xác định điều kiện dễ bay hơi không chỉ các kiểu nguyên thủy mà còn cả các kiểu do người dùng xác định. Trong trường hợp đó, variable sửa đổi kiểu theo cách tương tự như const. (Bạn cũng có thể áp dụng hằng số và biến đổi đồng thời cho cùng một loại.)

Không giống như const, biến động phân biệt giữa kiểu nguyên thủy và kiểu do người dùng định nghĩa. Cụ thể, không giống như các lớp, các kiểu nguyên thủy vẫn hỗ trợ tất cả các hoạt động của chúng (cộng, nhân, gán, v.v.) khi đủ điều kiện biến động. Ví dụ, bạn có thể gán một int không bay hơi cho một int dễ bay hơi, nhưng bạn không thể gán một đối tượng không bay hơi cho một đối tượng dễ bay hơi.

Hãy minh họa cách hoạt động của biến động trên các kiểu do người dùng xác định trên một ví dụ.

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

Nếu bạn nghĩ rằng tính dễ bay hơi không hữu ích với các đồ vật, hãy chuẩn bị cho một số bất ngờ.

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

Việc chuyển đổi từ một loại không đủ điều kiện thành một đối tác dễ bay hơi của nó là không đáng kể. Tuy nhiên, cũng như với const, bạn không thể thực hiện chuyến đi từ dễ bay sang không đủ điều kiện. Bạn phải sử dụng dàn diễn viên:

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

Một lớp đủ điều kiện dễ bay hơi chỉ cấp quyền truy cập vào một tập con của giao diện của nó, một tập con nằm dưới sự kiểm soát của người triển khai lớp. Người dùng có thể có toàn quyền truy cập vào giao diện của loại đó chỉ bằng cách sử dụng const_cast. Ngoài ra, cũng giống như hằng số, tính dễ bay hơi truyền từ lớp đến các thành viên của nó (ví dụ: variableGadget.name_ và variableGadget.state_ là các biến dễ bay hơi).

Biến động, Phần quan trọng và Điều kiện cuộc đua

Thiết bị đồng bộ hóa đơn giản nhất và thường được sử dụng nhất trong các chương trình đa luồng là mutex. Một mutex cho thấy các nguyên thủy Thu nhận và Giải phóng. Khi bạn gọi Acquire trong một chuỗi nào đó, bất kỳ chuỗi nào khác đang gọi Acquire sẽ bị chặn. Sau đó, khi chuỗi đó gọi Release, chính xác một chuỗi bị chặn trong cuộc gọi Mua lại sẽ được giải phóng. Nói cách khác, đối với một mutex nhất định, chỉ một luồng có thể có được thời gian của bộ xử lý trong khoảng thời gian giữa lệnh gọi Mua và lệnh phát hành. Đoạn mã thực thi giữa lệnh gọi Mua lại và lệnh gọi Phát hành được gọi là phần quan trọng. (Thuật ngữ Windows hơi khó hiểu vì nó gọi bản thân mutex là một phần quan trọng, trong khi "mutex" thực sự là một mutex liên tiến trình. Sẽ rất tuyệt nếu chúng được gọi là mutex luồng và xử lý mutex.)

Mutexes được sử dụng để bảo vệ dữ liệu chống lại các điều kiện chủng tộc. Theo định nghĩa, một điều kiện chạy đua xảy ra khi ảnh hưởng của nhiều luồng hơn đối với dữ liệu phụ thuộc vào cách các luồng được lập lịch. Điều kiện cuộc đua xuất hiện khi hai hoặc nhiều chủ đề cạnh tranh để sử dụng cùng một dữ liệu. Bởi vì các luồng có thể ngắt nhau tại những thời điểm tùy ý trong thời gian, dữ liệu có thể bị hỏng hoặc hiểu sai. Do đó, các thay đổi và đôi khi quyền truy cập vào dữ liệu phải được bảo vệ cẩn thận với các phần quan trọng. Trong lập trình hướng đối tượng, điều này thường có nghĩa là bạn lưu trữ một mutex trong một lớp dưới dạng một biến thành viên và sử dụng nó bất cứ khi nào bạn truy cập trạng thái của lớp đó.

Các lập trình viên đa luồng có kinh nghiệm có thể đã ngáp khi đọc hai đoạn trên, nhưng mục đích của họ là cung cấp một bài tập trí tuệ, bởi vì bây giờ chúng ta sẽ liên kết với kết nối dễ bay hơi. Chúng tôi thực hiện điều này bằng cách vẽ một song song giữa thế giới các loại C ++ và thế giới ngữ nghĩa luồng.

  • Bên ngoài một phần quan trọng, bất kỳ chuỗi nào cũng có thể làm gián đoạn bất kỳ phần nào khác bất kỳ lúc nào; không có sự kiểm soát, do đó, các biến có thể truy cập từ nhiều luồng rất dễ bay hơi. Điều này phù hợp với mục đích ban đầu của biến động - đó là ngăn trình biên dịch vô tình lưu trữ các giá trị được sử dụng bởi nhiều luồng cùng một lúc.
  • Bên trong phần quan trọng được xác định bởi mutex, chỉ một luồng có quyền truy cập. Do đó, bên trong một phần quan trọng, mã thực thi có ngữ nghĩa đơn luồng. Biến được kiểm soát không còn dễ bay hơi nữa - bạn có thể xóa bộ định tính dễ bay hơi.

Nói tóm lại, dữ liệu được chia sẻ giữa các luồng là khái niệm dễ bay hơi bên ngoài phần quan trọng và không dễ bay hơi bên trong phần quan trọng.

Bạn nhập một phần quan trọng bằng cách khóa mutex. Bạn xóa bộ định tính biến động khỏi một loại bằng cách áp dụng const_cast. Nếu chúng ta quản lý để kết hợp hai hoạt động này với nhau, chúng ta sẽ tạo ra một kết nối giữa hệ thống kiểu của C ++ và ngữ nghĩa phân luồng của ứng dụng. Chúng tôi có thể làm cho trình biên dịch kiểm tra điều kiện cuộc đua cho chúng tôi.

KhóaPtr

Chúng tôi cần một công cụ thu thập chuyển đổi mutex và const_cast. Hãy phát triển một mẫu lớp LockingPtr mà bạn khởi tạo với một đối tượng biến động obj và mutex mtx. Trong suốt thời gian tồn tại của nó, một LockingPtr giữ cho mtx có được. Ngoài ra, LockingPtr cung cấp quyền truy cập vào đối tượng bị loại bỏ dễ bay hơi. Quyền truy cập được cung cấp theo kiểu con trỏ thông minh, thông qua toán tử-> và toán tử *. Const_cast được thực hiện bên trong LockingPtr. Việc ép kiểu hợp lệ về mặt ngữ nghĩa vì LockingPtr giữ mutex có được trong suốt thời gian tồn tại của nó.

Đầu tiên, hãy xác định khung của một lớp Mutex mà LockingPtr sẽ hoạt động:

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

Để sử dụng LockingPtr, bạn triển khai Mutex bằng cách sử dụng các cấu trúc dữ liệu gốc và các hàm nguyên thủy của hệ điều hành.

LockingPtr được tạo mẫu với kiểu của biến được điều khiển. Ví dụ: nếu bạn muốn kiểm soát một Widget, bạn sử dụng một LockingPtr mà bạn khởi tạo với một biến loại Widget dễ bay hơi.

Định nghĩa của LockingPtr rất đơn giản. LockingPtr triển khai một con trỏ thông minh không phức tạp. Nó chỉ tập trung vào việc thu thập một const_cast và một phần quan trọng.

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

Mặc dù đơn giản, LockingPtr là một công cụ hỗ trợ rất hữu ích trong việc viết mã đa luồng chính xác. Bạn nên xác định các đối tượng được chia sẻ giữa các luồng là dễ bay hơi và không bao giờ sử dụng const_cast với chúng - luôn sử dụng các đối tượng tự động LockingPtr. Hãy minh họa điều này bằng một ví dụ.

Giả sử bạn có hai luồng dùng chung một đối tượng vectơ:

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

Bên trong một hàm luồng, bạn chỉ cần sử dụng một LockingPtr để có quyền truy cập có kiểm soát vào biến buffer_ member:

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

Mã này rất dễ viết và dễ hiểu - bất cứ khi nào bạn cần sử dụng buffer_, bạn phải tạo một LockingPtr trỏ đến nó. Khi bạn làm điều đó, bạn có quyền truy cập vào toàn bộ giao diện của vector.

Phần hay là nếu bạn mắc lỗi, trình biên dịch sẽ chỉ ra:

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

Bạn không thể truy cập bất kỳ chức năng nào của buffer_ cho đến khi bạn áp dụng const_cast hoặc sử dụng LockingPtr. Sự khác biệt là LockingPtr cung cấp một cách có thứ tự để áp dụng const_cast cho các biến dễ bay hơi.

LockingPtr là biểu cảm đáng kể. Nếu bạn chỉ cần gọi một hàm, bạn có thể tạo một đối tượng LockingPtr tạm thời không tên và sử dụng trực tiếp:

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

Quay lại các loại nguyên thủy

Chúng tôi đã thấy cách dễ dàng thay đổi bảo vệ các đối tượng khỏi truy cập không kiểm soát và cách LockingPtr cung cấp một cách đơn giản và hiệu quả để viết mã an toàn theo luồng. Bây giờ chúng ta hãy quay trở lại các kiểu nguyên thủy, được xử lý khác nhau bằng biến động.

Hãy xem xét một ví dụ trong đó nhiều luồng chia sẻ một biến kiểu int.

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

Nếu Tăng và Giảm được gọi từ các luồng khác nhau, thì phân đoạn trên là lỗi. Đầu tiên, ctr_ phải dễ bay hơi. Thứ hai, ngay cả một phép toán có vẻ nguyên tử như ++ ctr_ cũng thực sự là một phép toán ba giai đoạn. Bộ nhớ tự nó không có khả năng số học. Khi tăng một biến, bộ xử lý:

  • Đọc biến đó trong một thanh ghi
  • Tăng giá trị trong thanh ghi
  • Ghi kết quả trở lại bộ nhớ

Thao tác ba bước này được gọi là RMW (Read-Modify-Write). Trong phần Sửa đổi của hoạt động RMW, hầu hết các bộ xử lý giải phóng bus bộ nhớ để cấp cho các bộ xử lý khác quyền truy cập vào bộ nhớ.

Nếu tại thời điểm đó một bộ xử lý khác thực hiện một hoạt động RMW trên cùng một biến, chúng ta có một điều kiện đua: lần ghi thứ hai sẽ ghi đè lên tác động của lần đầu tiên.

Để tránh điều đó, một lần nữa, bạn có thể dựa vào LockingPtr:

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

Bây giờ mã là chính xác, nhưng chất lượng của nó kém hơn khi so sánh với mã của SyncBuf. Tại sao? Vì với Counter, trình biên dịch sẽ không cảnh báo nếu bạn truy cập nhầm vào ctr_ trực tiếp (mà không cần khóa). Trình biên dịch biên dịch ++ ctr_ nếu ctr_ dễ bay hơi, mặc dù mã được tạo đơn giản là không chính xác. Trình biên dịch không còn là đồng minh của bạn nữa, và chỉ sự chú ý của bạn mới có thể giúp bạn tránh được các điều kiện về chủng tộc.

Bạn nên làm gì tiếp theo? Đơn giản chỉ cần đóng gói dữ liệu nguyên thủy mà bạn sử dụng trong các cấu trúc cấp cao hơn và sử dụng dễ bay hơi với các cấu trúc đó. Nghịch lý là, sẽ tệ hơn nếu sử dụng trực tiếp dễ bay hơi với các bản cài sẵn, mặc dù thực tế ban đầu đây là mục đích sử dụng của dễ bay hơi!

Chức năng thành viên dễ thay đổi

Cho đến nay, chúng tôi đã có các lớp tổng hợp các thành viên dữ liệu dễ bay hơi; bây giờ chúng ta hãy nghĩ đến việc thiết kế các lớp sẽ là một phần của các đối tượng lớn hơn và được chia sẻ giữa các luồng. Đây là nơi mà các hàm thành viên dễ bay hơi có thể giúp ích rất nhiều.

Khi thiết kế lớp của mình, bạn chỉ đủ tiêu chuẩn biến động đối với những hàm thành viên an toàn cho luồng. Bạn phải giả định rằng mã từ bên ngoài sẽ gọi các hàm biến động từ mã bất kỳ lúc nào. Đừng quên: dễ bay hơi bằng mã đa luồng miễn phí và không có phần quan trọng; không bay hơi bằng kịch bản đơn luồng hoặc bên trong một phần quan trọng.

Ví dụ: bạn xác định một Widget lớp thực hiện một hoạt động trong hai biến thể - một biến thể an toàn theo luồng và một biến thể nhanh, không được bảo vệ.

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

Lưu ý việc sử dụng quá tải. Giờ đây, người dùng của Widget có thể gọi Thao tác bằng một cú pháp thống nhất cho các đối tượng dễ bay hơi và có được sự an toàn của luồng, hoặc đối với các đối tượng thông thường và có được tốc độ. Người dùng phải cẩn thận về việc xác định các đối tượng Widget được chia sẻ là dễ bay hơi.

Khi thực hiện một chức năng thành viên dễ bay hơi, thao tác đầu tiên thường là khóa chức năng này bằng LockingPtr. Sau đó, công việc được thực hiện bằng cách sử dụng anh chị em không bay hơi:

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

Tóm lược

Khi viết các chương trình đa luồng, bạn có thể sử dụng dễ bay hơi để có lợi cho mình. Bạn phải tuân thủ các quy tắc sau:

  • Xác định tất cả các đối tượng được chia sẻ là dễ bay hơi.
  • Không sử dụng trực tiếp dễ bay hơi với các loại nguyên thủy.
  • Khi xác định các lớp chia sẻ, hãy sử dụng các hàm thành viên dễ bay hơi để thể hiện sự an toàn của luồng.

Nếu bạn làm điều này và nếu bạn sử dụng thành phần chung đơn giản LockingPtr, bạn có thể viết mã an toàn cho luồng và ít lo lắng hơn về điều kiện chủng tộc, bởi vì trình biên dịch sẽ lo lắng cho bạn và sẽ chăm chỉ chỉ ra những điểm bạn sai.

Một vài dự án tôi đã tham gia sử dụng dễ bay hơi và LockingPtr mang lại hiệu quả tuyệt vời. Mã rõ ràng và dễ hiểu. Tôi nhớ lại một vài deadlock, nhưng tôi thích deadlocks hơn là các điều kiện chạy đua vì chúng dễ gỡ lỗi hơn rất nhiều. Hầu như không có vấn đề gì liên quan đến điều kiện chủng tộc. Nhưng sau đó bạn không bao giờ biết.

Sự nhìn nhận

Rất cảm ơn James Kanze và Sorin Jianu, những người đã giúp đưa ra những ý tưởng sâu sắc.


Andrei Alexandrescu là Giám đốc phát triển tại RealNetworks Inc. (www.realnetworks.com), có trụ sở tại Seattle, WA và là tác giả của cuốn sách nổi tiếng Thiết kế C ++ hiện đại. Anh ta có thể được liên hệ tại www.moderncppdesign.com. Andrei cũng là một trong những giảng viên tiêu biểu của The C ++ Seminar (www.gotw.ca/cpp_seminar).

Bài viết này có thể hơi cũ, nhưng nó cung cấp cái nhìn sâu sắc về việc sử dụng tuyệt vời công cụ sửa đổi biến động trong việc sử dụng lập trình đa luồng để giúp giữ cho các sự kiện không đồng bộ trong khi trình biên dịch kiểm tra điều kiện chủng tộc cho chúng tôi. Điều này có thể không trực tiếp trả lời câu hỏi ban đầu của OP về việc tạo hàng rào bộ nhớ, nhưng tôi chọn đăng điều này như một câu trả lời cho những người khác như một tài liệu tham khảo tuyệt vời hướng tới việc sử dụng tốt biến động khi làm việc với các ứng dụng đa luồng.


0

Từ khóa volatilevề cơ bản có nghĩa là việc đọc và ghi một đối tượng phải được thực hiện chính xác như được viết bởi chương trình và không được tối ưu hóa theo bất kỳ cách nào . Mã nhị phân phải tuân theo mã C hoặc C ++: tải ở đó dữ liệu này được đọc, lưu trữ nơi có ghi.

Điều đó cũng có nghĩa là không cần đọc sẽ dẫn đến một giá trị có thể dự đoán được: trình biên dịch không nên giả định bất kỳ điều gì về một lần đọc ngay cả ngay sau khi ghi vào cùng một đối tượng dễ bay hơi:

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatilecó thể là công cụ quan trọng nhất trong hộp công cụ "C là ngôn ngữ hợp ngữ cấp cao" .

Việc khai báo một đối tượng dễ bay hơi có đủ để đảm bảo hành vi của mã xử lý các thay đổi không đồng bộ hay không phụ thuộc vào nền tảng: các CPU khác nhau cung cấp các mức đồng bộ hóa đảm bảo khác nhau cho việc đọc và ghi bộ nhớ bình thường. Bạn có thể không nên cố gắng viết mã đa luồng cấp thấp như vậy trừ khi bạn là một chuyên gia trong lĩnh vực này.

Các nguyên thủy nguyên tử cung cấp chế độ xem các đối tượng ở cấp độ cao hơn đẹp mắt để đa luồng giúp dễ dàng suy luận về mã. Hầu như tất cả các lập trình viên nên sử dụng nguyên thủy nguyên tử hoặc nguyên thủy cung cấp loại trừ lẫn nhau như mutexes, khóa đọc-ghi, bán nghĩa hoặc các nguyên thủy chặn khác.

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.