Là lời giải thích của trật tự thoải mái sai lầm trong cppreference?


13

Trong tài liệu của std::memory_ordertrên cppreference.com có một ví dụ về việc đặt hàng thoải mái:

Đặt hàng thoải mái

Các hoạt động nguyên tử được gắn thẻ memory_order_relaxedkhông phải là hoạt động đồng bộ hóa; họ không áp đặt một trật tự trong số các truy cập bộ nhớ đồng thời. Họ chỉ đảm bảo tính nguyên tử và tính nhất quán để sửa đổi.

Ví dụ: với x và y ban đầu bằng 0,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

được phép tạo r1 == r2 == 42 bởi vì, mặc dù A được giải trình tự trước B trong luồng 1 và C được giải trình tự trước D trong luồng 2, không có gì ngăn D xuất hiện trước A theo thứ tự sửa đổi của y và B từ xuất hiện trước C theo thứ tự sửa đổi của x. Hiệu ứng phụ của D trên y có thể hiển thị với tải A trong luồng 1 trong khi hiệu ứng phụ của B trên x có thể hiển thị với tải C trong luồng 2. Đặc biệt, điều này có thể xảy ra nếu D hoàn thành trước C trong luồng 2, do sắp xếp lại trình biên dịch hoặc trong thời gian chạy.

nó nói "C được giải trình tự trước D trong luồng 2".

Theo định nghĩa của trình tự trước, có thể tìm thấy trong Thứ tự đánh giá , nếu A được giải trình tự trước B, thì việc đánh giá A sẽ được hoàn thành trước khi bắt đầu đánh giá B. Vì C được giải trình tự trước D trong luồng 2, C phải được hoàn thành trước khi D bắt đầu, do đó phần điều kiện của câu cuối cùng của ảnh chụp sẽ không bao giờ được thỏa mãn.


Là câu hỏi của bạn cụ thể về C ++ 11?
tò mò

không, nó cũng áp dụng cho c ++ 14,17. Tôi biết cả trình biên dịch và CPU có thể sắp xếp lại C với D. Nhưng nếu việc sắp xếp lại xảy ra, C không thể hoàn thành trước khi D bắt đầu. Vì vậy, tôi nghĩ rằng có một thuật ngữ - sử dụng sai trong câu "A được xâu chuỗi - trước B trong luồng 1 và C được xâu chuỗi trước D trong luồng 2". Nói chính xác hơn là "Trong mã, A được PLACED TRƯỚC B trong luồng 1 và C được PLACED TRƯỚC D trong luồng 2". Mục đích của câu hỏi này là để xác nhận suy nghĩ này
abigaile

Không có gì được định nghĩa theo thuật ngữ "sắp xếp lại".
tò mò

Câu trả lời:


13

Tôi tin rằng cppreference là đúng. Tôi nghĩ rằng điều này thực hiện theo quy tắc "as-if" [intro.execut] / 1 . Trình biên dịch chỉ bị ràng buộc để tái tạo hành vi có thể quan sát được của chương trình được mô tả bởi mã của bạn. Một mối quan hệ tuần tự trước chỉ được thiết lập giữa các đánh giá theo quan điểm của luồng trong đó các đánh giá này được thực hiện [intro.execut] / 15 . Điều đó có nghĩa là khi hai đánh giá lần lượt nối tiếp nhau xuất hiện ở đâu đó trong một số luồng, mã thực sự chạy trong luồng đó phải hoạt động như thể bất cứ đánh giá đầu tiên nào thực sự ảnh hưởng đến bất kỳ đánh giá thứ hai nào. Ví dụ

int x = 0;
x = 42;
std::cout << x;

phải in 42. Tuy nhiên, trình biên dịch không thực sự phải lưu giá trị 42 vào một đối tượng xtrước khi đọc lại giá trị từ đối tượng đó để in nó. Cũng có thể nhớ rằng giá trị cuối cùng được lưu trữ xlà 42 và sau đó chỉ cần in trực tiếp giá trị 42 trước khi thực hiện lưu trữ thực tế của giá trị 42 đến x. Trong thực tế, nếu xlà một biến cục bộ, nó cũng có thể chỉ theo dõi giá trị nào của biến đó được gán lần cuối tại bất kỳ điểm nào và thậm chí không bao giờ tạo một đối tượng hoặc thực sự lưu trữ giá trị 42. Không có cách nào để luồng cho biết sự khác biệt. Hành vi sẽ luôn như thể có một biến và như thể giá trị 42 thực sự được lưu trữ trong một đối tượng x trước đóđược tải từ đối tượng đó. Nhưng điều đó không có nghĩa là mã máy được tạo phải thực sự lưu trữ và tải bất cứ thứ gì ở bất cứ đâu. Tất cả những gì được yêu cầu là hành vi có thể quan sát được của mã máy được tạo ra không thể phân biệt được với hành vi sẽ là gì nếu tất cả những điều này thực sự xảy ra.

Nếu chúng ta nhìn vào

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

sau đó, C được giải trình tự trước D. Nhưng khi được xem từ chủ đề này một cách cô lập, không có gì C làm ảnh hưởng đến kết quả của D. Và không có gì mà D làm thay đổi kết quả của C. Cách duy nhất người ta có thể ảnh hưởng đến người khác là như một hậu quả gián tiếp của một cái gì đó xảy ra trong một chủ đề khác. Tuy nhiên, bằng cách chỉ định std::memory_order_relaxed, bạn tuyên bố rõ ràngrằng thứ tự mà tải và lưu trữ được quan sát bởi một luồng khác là không liên quan. Vì không có luồng nào khác có thể quan sát tải và lưu trữ theo bất kỳ thứ tự cụ thể nào, nên không có luồng nào khác có thể làm cho C và D ảnh hưởng lẫn nhau một cách nhất quán. Do đó, thứ tự tải và lưu trữ thực sự được thực hiện là không liên quan. Vì vậy, trình biên dịch là miễn phí để sắp xếp lại chúng. Và, như đã đề cập trong phần giải thích bên dưới ví dụ đó, nếu việc lưu trữ từ D được thực hiện trước khi tải từ C, thì r1 == r2 == 42 thực sự có thể đến với Riêu


Vì vậy, về cơ bản các trạng thái tiêu chuẩn mà C phải xảy ra trước D , nhưng trình biên dịch tin rằng không thể chứng minh được liệu C hay D có xảy ra tiếp theo hay không, do quy tắc as-if, sắp xếp lại chúng, đúng không?
Fureeish

4
@Fureeish Số C phải xảy ra trước D theo như chủ đề mà chúng xảy ra có thể nói. Quan sát từ bối cảnh khác có thể không phù hợp với quan điểm đó.
Ded repeatator


1
@cquilguy Michael đã đăng một lời giải thích dài về nó cùng với các liên kết đến các chương có liên quan trong tiêu chuẩn.
Các cuộc đua nhẹ nhàng trong quỹ đạo

2
@curiousguy Các tiêu chuẩn thực hiện một nhãn của nó quy định "sự cai trị như nếu" trong một chú thích: "Quy định này đôi khi được gọi là‘như nếu’quy tắc" intro.execution
Caleth

1

Đôi khi có thể một hành động được ra lệnh liên quan đến hai chuỗi hành động khác, mà không ngụ ý bất kỳ trật tự tương đối nào của các hành động trong các chuỗi đó liên quan đến nhau.

Giả sử, ví dụ, một người có ba sự kiện sau:

  • lưu trữ 1 đến p1
  • tải p2 vào temp
  • lưu trữ 2 đến p3

và việc đọc p2 được sắp xếp độc lập sau khi viết p1 và trước khi viết p3, nhưng không có thứ tự cụ thể nào trong đó cả p1 và p3 đều tham gia. Tùy thuộc vào những gì được thực hiện với p2, trình biên dịch có thể trì hoãn p1 qua p3 và vẫn đạt được ngữ nghĩa cần thiết với p2. Tuy nhiên, giả sử, trình biên dịch biết rằng đoạn mã trên là một phần của chuỗi lớn hơn:

  • lưu trữ 1 đến p2 [tuần tự trước khi tải p2]
  • [làm như trên]
  • lưu trữ 3 vào p1 [tuần tự sau các cửa hàng khác đến p1]

Trong trường hợp đó, nó có thể xác định rằng nó có thể sắp xếp lại cửa hàng thành p1 sau đoạn mã trên và hợp nhất nó với cửa hàng sau, do đó dẫn đến mã viết p3 mà không cần viết p1 trước:

  • đặt tạm thời thành 1
  • lưu trữ tạm thời đến p2
  • lưu trữ 2 đến p3
  • lưu trữ 3 đến p1

Mặc dù có vẻ như sự phụ thuộc dữ liệu sẽ khiến các phần nhất định của các mối quan hệ tuần tự hành xử quá cảnh, một trình biên dịch có thể xác định các tình huống mà sự phụ thuộc dữ liệu rõ ràng không tồn tại, và do đó sẽ không có hiệu ứng bắc cầu mà người ta mong đợi.


1

Nếu có hai câu lệnh, trình biên dịch sẽ tạo mã theo thứ tự tuần tự để mã cho câu lệnh đầu tiên sẽ được đặt trước câu lệnh thứ hai. Nhưng cpus trong nội bộ có đường ống dẫn và có thể chạy các hoạt động lắp ráp song song. Câu lệnh C là một lệnh tải. Trong khi bộ nhớ đang được tìm nạp, đường ống sẽ xử lý một vài lệnh tiếp theo và do chúng không phụ thuộc vào lệnh tải, cuối cùng chúng có thể được thực thi trước khi C kết thúc (ví dụ: dữ liệu cho D nằm trong bộ nhớ cache, C trong bộ nhớ chính).

Nếu người dùng thực sự cần hai câu lệnh được thực thi tuần tự, các hoạt động sắp xếp bộ nhớ chặt chẽ hơn có thể được sử dụng. Nói chung, người dùng không quan tâm miễn là chương trình đúng.


-9

Bất cứ điều gì bạn nghĩ là hợp lệ như nhau. Tiêu chuẩn không nói những gì thực thi tuần tự, những gì không và làm thế nào để trộn lẫn nó .

Tùy thuộc vào bạn, và với mỗi lập trình viên duy nhất, để tạo nên một ngữ nghĩa nhất quán trên đỉnh của mớ hỗn độn đó, một công việc xứng đáng với nhiều tiến sĩ.

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.