Điều gì được đảm bảo với C ++ std :: nguyên tử ở cấp độ lập trình viên?


9

Tôi đã nghe và đọc một số bài báo, các bài nói chuyện và các câu hỏi về stackoverflow std::atomic, và tôi muốn chắc chắn rằng tôi đã hiểu rõ về nó. Bởi vì tôi vẫn còn một chút nhầm lẫn với khả năng ghi dòng bộ đệm do sự chậm trễ có thể xảy ra trong các giao thức kết nối bộ đệm MESI (hoặc dẫn xuất), lưu trữ bộ đệm, hàng đợi không hợp lệ, v.v.

Tôi đọc x86 có mô hình bộ nhớ mạnh hơn và nếu việc vô hiệu hóa bộ đệm bị trì hoãn thì x86 có thể hoàn nguyên các hoạt động đã bắt đầu. Nhưng bây giờ tôi chỉ quan tâm đến những gì tôi nên giả sử là một lập trình viên C ++, độc lập với nền tảng.

[T1: thread1 T2: thread2 V1: biến nguyên tử dùng chung]

Tôi hiểu rằng std :: nguyên tử đảm bảo rằng,

(1) Không có cuộc đua dữ liệu nào xảy ra trên một biến (nhờ truy cập độc quyền vào dòng bộ đệm).

(2) Tùy thuộc vào bộ nhớ mà chúng tôi sử dụng, nó đảm bảo (với các rào cản) rằng tính nhất quán tuần tự xảy ra (trước một rào cản, sau một rào cản hoặc cả hai).

(3) Sau khi ghi nguyên tử (V1) trên T1, một nguyên tử RMW (V1) trên T2 sẽ được kết hợp (dòng bộ đệm của nó sẽ được cập nhật với giá trị ghi trên T1).

Nhưng như đề cập đến bộ đệm kết hợp bộ đệm ,

Hàm ý của tất cả những điều này là, theo mặc định, tải có thể tìm nạp dữ liệu cũ (nếu một yêu cầu vô hiệu tương ứng đang ngồi trong hàng đợi không hợp lệ)

Vậy, điều nào sau đây là đúng?

(4) std::atomicKHÔNG đảm bảo rằng T2 sẽ không đọc giá trị 'cũ' trên số đọc nguyên tử (V) sau khi viết nguyên tử (V) trên T1.

Câu hỏi nếu (4) là đúng: nếu nguyên tử ghi trên T1 làm mất hiệu lực dòng bộ đệm bất kể độ trễ, tại sao T2 chờ hiệu lực có hiệu lực khi hoạt động của RMW nguyên tử nhưng không đọc trên nguyên tử?

Câu hỏi nếu (4) sai: khi nào thì một chủ đề có thể đọc giá trị 'cũ' và "nó hiển thị" trong khi thực hiện, sau đó?

Tôi đánh giá cao câu trả lời của bạn rất nhiều

Cập nhật 1

Vì vậy, có vẻ như tôi đã sai trên (3) rồi. Hãy tưởng tượng xen kẽ sau đây, với giá trị ban đầu là V1 = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Mặc dù RMW của T2 được đảm bảo xảy ra hoàn toàn sau W (1) trong trường hợp này, nó vẫn có thể đọc giá trị 'cũ' (tôi đã sai). Theo đó, nguyên tử không đảm bảo tính đồng nhất bộ nhớ cache đầy đủ, chỉ có tính nhất quán tuần tự.

Cập nhật 2

(5) Bây giờ hãy tưởng tượng ví dụ này (x = y = 0 và là nguyên tử):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

theo những gì chúng ta đã nói, việc nhìn thấy "thông điệp" được hiển thị trên màn hình sẽ không cung cấp cho chúng ta thông tin ngoài việc T2 được thực hiện sau T1. Vì vậy, một trong những vụ hành quyết sau đây có thể đã xảy ra:

  • T1 <T3 <T2
  • T1 <T2 <T3 (trong đó T3 thấy x = 1 nhưng chưa có y = 1)

Có đúng không?

(6) Nếu một luồng luôn có thể đọc các giá trị 'cũ', điều gì sẽ xảy ra nếu chúng ta lấy kịch bản "xuất bản" điển hình nhưng thay vì báo hiệu rằng một số dữ liệu đã sẵn sàng, chúng ta sẽ làm ngược lại (xóa dữ liệu)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

trong đó T2 vẫn sẽ sử dụng ptr bị xóa cho đến khi thấy is_enables là sai.

(7) Ngoài ra, thực tế là các luồng có thể đọc các giá trị 'cũ' có nghĩa là một mutex có thể được thực hiện chỉ với một nguyên tử không khóa phải không? Nó sẽ đòi hỏi một cơ chế đồng bộ giữa các chủ đề. Nó sẽ yêu cầu một nguyên tử có thể khóa?

Câu trả lời:


3
  1. Có, không có cuộc đua dữ liệu
  2. Có, với các memory_ordergiá trị phù hợp, bạn có thể đảm bảo tính nhất quán tuần tự
  3. Một nguyên tử đọc-sửa đổi-ghi sẽ luôn luôn xảy ra hoàn toàn trước hoặc hoàn toàn sau khi một nguyên tử ghi vào cùng một biến
  4. Có, T2 có thể đọc giá trị cũ từ một biến sau khi viết nguyên tử trên T1

Các hoạt động đọc-sửa đổi-ghi nguyên tử được chỉ định theo cách để đảm bảo tính nguyên tử của chúng. Nếu một luồng khác có thể ghi vào giá trị sau lần đọc đầu tiên và trước khi ghi hoạt động của RMW, thì hoạt động đó sẽ không phải là nguyên tử.

Chủ đề luôn có thể đọc các giá trị cũ, trừ khi xảy ra - trước khi đảm bảo thứ tự tương đối .

Nếu một hoạt động của RMW đọc một giá trị "cũ", thì nó đảm bảo rằng ghi mà nó tạo ra sẽ hiển thị trước khi bất kỳ ghi nào từ các luồng khác sẽ ghi đè lên giá trị mà nó đọc.

Cập nhật ví dụ

Nếu T1 ghi x=1và T2 thực hiện x++, với xban đầu là 0, các lựa chọn theo quan điểm lưu trữ của xlà:

  1. Viết của T1 là đầu tiên, do đó, T1 viết x=1, sau đó đọc T2 x==1, tăng lên 2 và ghi lại x=2dưới dạng một hoạt động nguyên tử.

  2. Viết của T1 là thứ hai. T2 đọc x==0, tăng nó lên 1 và ghi lại x=1dưới dạng một thao tác, sau đó ghi T1 x=1.

Tuy nhiên, với điều kiện là không có các điểm đồng bộ hóa khác giữa hai luồng này, các luồng có thể tiến hành các thao tác không được xóa vào bộ nhớ.

Do đó, T1 có thể phát hành x=1, sau đó tiến hành những thứ khác, mặc dù T2 vẫn sẽ đọc x==0(và do đó viết x=1).

Nếu có bất kỳ điểm đồng bộ hóa nào khác thì rõ ràng luồng nào được sửa đổi xtrước, bởi vì các điểm đồng bộ hóa đó sẽ buộc một đơn đặt hàng.

Điều này là rõ ràng nhất nếu bạn có một điều kiện về giá trị được đọc từ một hoạt động của RMW.

Cập nhật 2

  1. Nếu bạn sử dụng memory_order_seq_cst(mặc định) cho tất cả các hoạt động nguyên tử, bạn không cần phải lo lắng về loại điều này. Từ quan điểm của chương trình, nếu bạn thấy "thông điệp" thì T1 chạy, rồi T3, rồi T2.

Nếu bạn sử dụng các thứ tự bộ nhớ khác (đặc biệt memory_order_relaxed) thì bạn có thể thấy các tình huống khác trong mã của mình.

  1. Trong trường hợp này, bạn có một lỗi. Giả sử is_enabledcờ là đúng, khi T2 đi vào whilevòng lặp của nó , vì vậy nó quyết định chạy phần thân. Bây giờ T1 xóa dữ liệu và T2 sau đó trì hoãn con trỏ, đó là một con trỏ lơ lửng và hành vi không xác định xảy ra sau đó. Các nguyên tử không giúp đỡ hoặc cản trở theo bất kỳ cách nào ngoài việc ngăn chặn cuộc đua dữ liệu trên cờ.

  2. Bạn có thể thực hiện một mutex với một biến nguyên tử duy nhất.


Cảm ơn rất nhiều @Anthony Wiliams cho câu trả lời nhanh chóng của bạn. Tôi đã cập nhật câu hỏi của mình với một ví dụ về việc RMW đọc giá trị 'cũ'. Nhìn vào ví dụ này, ý của bạn là gì khi đặt hàng tương đối và W (1) của T2 sẽ hiển thị trước khi ghi bất kỳ? Điều đó có nghĩa là một khi T2 đã thấy những thay đổi của T1 thì nó sẽ không đọc W (1) của T2 nữa?
Albert Caldas

Vì vậy, nếu "Chủ đề luôn có thể đọc các giá trị cũ" thì có nghĩa là độ liên kết bộ đệm không bao giờ được đảm bảo (ít nhất là ở cấp lập trình viên c ++). Bạn có thể xem update2 của tôi không?
Albert Caldas

Bây giờ tôi thấy rằng tôi nên chú ý nhiều hơn đến các mô hình bộ nhớ ngôn ngữ và phần cứng để hiểu đầy đủ tất cả, đó là phần tôi đang thiếu. cảm ơn rất nhiều!
Albert Caldas

1

Về (3) - nó phụ thuộc vào thứ tự bộ nhớ được sử dụng. Nếu cả hai, cửa hàng và hoạt động của RMW đều sử dụng std::memory_order_seq_cst, thì cả hai hoạt động đều được đặt hàng theo một cách nào đó - tức là, cửa hàng xảy ra trước RMW, hoặc ngược lại. Nếu cửa hàng được đặt hàng trước RMW, thì nó được đảm bảo rằng hoạt động của RMW "nhìn thấy" giá trị được lưu trữ. Nếu cửa hàng được đặt hàng sau RMW, nó sẽ ghi đè lên giá trị được ghi bởi hoạt động của RMW.

Nếu bạn sử dụng các đơn đặt hàng bộ nhớ thoải mái hơn, các sửa đổi sẽ vẫn được sắp xếp theo một cách nào đó (thứ tự sửa đổi của biến), nhưng bạn không đảm bảo liệu RMW có "nhìn thấy" giá trị từ hoạt động của cửa hàng hay không - ngay cả khi hoạt động của RMW là thứ tự sau khi viết theo thứ tự sửa đổi của biến.

Trong trường hợp bạn muốn đọc thêm một bài viết khác, tôi có thể giới thiệu bạn đến Mô hình bộ nhớ cho lập trình viên C / C ++ .


Cảm ơn về bài viết, tôi đã không đọc nó. Ngay cả khi nó khá cũ, việc kết hợp các ý tưởng của tôi lại rất hữu ích.
Albert Caldas

1
Vui mừng khi biết rằng - bài viết này là một chương được mở rộng và sửa đổi một chút từ luận án thạc sĩ của tôi. :-) Nó tập trung vào mô hình bộ nhớ như đã giới thiệu C ++ 11; Tôi có thể cập nhật nó để phản ánh những thay đổi (nhỏ) được giới thiệu trong C ++ 14/17. Xin vui lòng cho tôi biết nếu bạn có bất kỳ ý kiến ​​hoặc đề xuất cải tiến!
mpoeter
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.