[英]std::memory_order and instruction order, clarification
這是一個跟進問題這一個。
我想確切地弄清楚指令排序的含義,以及它如何受到std::memory_order_acquire
、 std::memory_order_release
等的影響......
在我鏈接的問題中,已經提供了一些細節,但我覺得所提供的答案並不是真正關於訂單(這更多是我要找的東西),而是有點激勵為什么這是必要的等等。
我將引用相同的示例作為參考
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
簡而言之,我想弄清楚這兩行的指令順序究竟發生了什么
ptr.store(p, std::memory_order_release);
和
while (!(p2 = ptr.load(std::memory_order_acquire)))
根據文檔關注第一個
...在此存儲之后,無法重新排序當前線程中的讀取或寫入...
我一直在看一些演講來理解這個排序問題,我明白為什么它現在很重要。 我還不太清楚編譯器如何翻譯訂單規范,我認為文檔中給出的示例也不是特別有用,因為在運行producer
的線程中的存儲操作之后沒有其他指令,因此什么都不會無論如何重新訂購。 但是也有可能我誤解了,他們是否可能意味着等效的組裝
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
將是這樣翻譯的前兩行將永遠不會在原子存儲之后移動? 同樣,在運行生產者的線程中,是否有可能在原子加載之前不會移動任何斷言(或等效的程序集)? 假設我在存儲之后有第三條指令,而不是那些已經在原子加載之后的指令會發生什么?
我也嘗試編譯這樣的代碼來保存帶有-S
標志的中間匯編代碼,但它非常大,我真的想不出來。
再次澄清,這個問題是關於如何排序的,而不是關於為什么這些機制有用或必要的。
我知道當談到內存排序時,人們通常會試圖爭論是否以及如何重新排序操作,但在我看來這是錯誤的方法! C++ 標准沒有說明如何重新排序指令,而是定義了happens-before 關系,它本身基於sequenced-before、synchronized-with 和inter-thread-happens-before 關系。
從 store-release 讀取值的獲取加載與該獲取加載同步,因此建立了一個發生在之前的關系。 由於happens-before 關系的傳遞性,在store-release 之前“sequenced-before”的操作,在acquire-load 之前也“happen-before”。 關於使用原子實現的正確性的任何論點都應始終依賴於發生之前的關系。 如果以及如何重新排序指令僅僅是應用發生在關系的規則的結果。
有關 C++ 內存模型的更詳細說明,您可以查看C/C++ 程序員的內存模型。
沒有原子:
std::string* ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr = p;
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
在producer
,編譯器可以在賦值給 ptr 之后自由地移動對 data 的賦值。 因為在設置數據之前 ptr 變為非空,所以可以觸發相應的斷言。
發布商店禁止編譯器這樣做。
在consumer
,編譯器可以自由地將數據上的斷言移動到循環之前。
load-acquire 禁止編譯器這樣做。
與排序無關,但編譯器可以自由地完全省略循環,因為如果循環開始時 ptr 為空,則沒有任何東西可以有效地使其顯示為非空,從而導致無限循環,也可以假定不會發生。
我認為文檔中給出的示例也不是特別有用,因為在運行生產者的線程中進行存儲操作之后,沒有其他指令,因此無論如何都不會重新排序。
如果有,無論如何都可以提前處決。 那怎么會痛?
生產者唯一必須保證的是在設置標志之前完全寫入內存中的“生產”; 否則消費者將無法避免讀取未初始化的內存(或對象的舊值)。
太晚設置已發布的對象將是災難性的。 但是如何“過早”開始設置另一個已發布的對象(比如第二個)是一個問題?
你怎么會知道制片人過早地做什么? 您唯一可以做的就是檢查標志,並且只有在設置標志后,您才能觀察已發布的對象。
因此,如果在修改標志之前重新排序了任何內容,您應該看不到它。
producer():
sub rsp, 8
mov edi, 32
call operator new(unsigned long)
mov DWORD PTR data[rip], 42
lea rdx, [rax+16]
mov DWORD PTR [rax+16], 1819043144
mov QWORD PTR [rax], rdx
mov BYTE PTR [rax+20], 111
mov QWORD PTR [rax+8], 5
mov BYTE PTR [rax+21], 0
mov QWORD PTR ptr[abi:cxx11][rip], rax
add rsp, 8
ret
(如果你想知道, ptr[abi:cxx11]
是一個裝飾名稱,而不是一些時髦的 asm 語法,所以ptr[abi:cxx11][rip]
意味着ptr[rip]
。)
可以總結為:
setup stack frame assign data setup string object assign ptr remove frame and return
所以真的沒有什么值得注意的,除了ptr
最后分配。
你必須選擇另一個目標才能看到更有趣的東西。
回答您的評論可能有用:
我仍然覺得我的問題不清楚,我的問題更像是以下內容。 假設(例如在生產者中)您在原子存儲之后添加更多語句,例如 data_2 = 175 和 data_3 = 10,其中 data_2 和 data_3 是全局變量。 現在重新訂購究竟受到了怎樣的影響? 我知道你可能在回答中提到了這一點,所以如果我很煩人,我深表歉意
讓我們擺弄你的producer()
void producer()
{
data = 41;
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
consumer()
能否在“數據”中找到值 41。 不。 42 的值已(邏輯上)存儲到發布柵欄點的數據中,如果consumer()
找到值 42,則 42 的存儲(至少看起來)將發生在發布柵欄之后。
好的,現在讓我們進一步修補......
void producer()
{
data = 0xFF01;
std::string* p = new std::string("Hello");
data = 0xFF02;
ptr.store(p, std::memory_order_release);
data = 0x0003
}
現在一切都結束了。 data
不是原子的,不能保證consumer
會找到什么。 在大多數架構中,實際情況是唯一的候選是 0xFF02 或 0x0003,但肯定有一些架構可能會找到 0xFF03 和/或 0x0002。 這可能發生在具有 8 位總線的架構上,如果 16 位int
被寫入為 2 個單字節操作(從任一“結束”)。
但原則上,現在根本無法保證面對這樣的數據競爭會存儲什么。 這是一場數據競爭,因為無法控制確保consumer
是否通過額外的寫入被訂購。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.