[英]Synchronising with mutex and relaxed memory order atomic
我有一個共享數據結構,它已經在內部與互斥體同步。 我可以使用帶有 memory 順序的原子來表示變化嗎? 我在代碼中的意思的一個非常簡化的視圖
線程 1
shared_conf.set("somekey","someval");
is_reconfigured.store(true, std::memory_order_relaxed);
線程 2
if (is_reconfigured.load(std::memory_order_relaxed)) {
inspect_shared_conf();
}
是否保證我會在 shared_map 中看到更新? 共享的 map 本身在內部使用互斥鎖同步每個寫/讀調用
您的示例代碼將起作用,是的,您將看到更新。 輕松的排序將為您提供正確的行為。 也就是說,就性能而言,它實際上可能不是最佳的。
讓我們看一個更具體的例子,其中互斥鎖是明確的。
std::mutex m;
std::atomic<bool> updated;
foo shared;
void thr1() {
m.lock();
shared.write(new_data);
m.unlock();
updated.store(true, std::memory_order_relaxed);
}
void thr2() {
if (updated.load(std::memory_order_relaxed)) {
m.lock();
data = shared.read();
m.unlock();
}
}
m.lock()
是獲取操作, m.unlock()
是釋放操作。 這意味着沒有什么可以阻止updated.store(true)
向上“浮動”到臨界區,超過m.unlock()
甚至shared.write()
。 乍一看這似乎很糟糕,因為updated
標志的全部意義在於發出shared.write()
已完成的信號。 但在那種情況下並沒有實際的傷害發生,因為 thr1 仍然持有互斥鎖m
,所以如果 thr2 開始嘗試讀取共享數據,它只會等到 thr1 丟棄它。
真正糟糕的是,如果 updated.store updated.store()
一直浮動到超過m.lock()
; 然后 thr2 可能會看到updated.load() == true
並在 thr1 之前獲取互斥鎖。 但是,由於獲取語義m.lock()
,這不會發生。
thr2
中可能存在一些相關問題(稍微復雜一些,因為它們必須是推測性的)但同樣的事實再次拯救了我們: updated.load()
可以向下沉入關鍵部分,但不能完全通過它(因為m.unlock()
是釋放)。
但在這個實例中, updated
操作的更強 memory 順序雖然看起來更昂貴,但實際上可能會提高性能。 如果updated
中的值true
過早可見,則 thr2 會在 m 已被 thr1 鎖定時嘗試鎖定m
,因此 thr2 在等待m
變為可用時將不得不阻塞。 但是如果改成updated.store(true, std::memory_order_release)
和updated.load updated.load(std::memory_order_acquire)
,那么updated
中的true
值只有在m
真正被thr1解鎖之后才會可見,m也是如此m.lock()
thr2 中的m.lock()
應始終立即成功(忽略可能存在的任何其他線程的爭用)。
好的,這是一個非正式的解釋,但我們知道在考慮 memory 訂購時這些總是有風險的。 讓我們從 C++ memory model 的正式規則中給出一個證明。我將遵循 C++20 標准,因為我手頭有它,但我不認為與 C++17 有任何重大的相關變化。參見 [intro.races]此處使用的術語的定義。
我聲稱,如果shared.read()
完全執行,那么shared.write(new_data)
會在它之前發生,因此通過寫讀一致性 [intro.races p18] shared.read()
將看到新數據。
m
上的鎖定和解鎖操作是完全有序的 [thread.mutex.requirements.mutex p5]。 考慮兩種情況:要么 thr1 的解鎖先於 thr2 的鎖定(情況 I),反之亦然(情況 II)。
案例一
如果在m
的鎖定順序中 thr1 的解鎖先於 thr2 的鎖定,則沒有問題; 它們相互同步[thread.mutex.requirements.mutex p11]。 由於shared.write(new_data)
在 thr1 的m.unlock()
之前排序,而 thr2 的m.lock()
在shared.read()
之前排序,通過追蹤 [intro.races] 中的定義,我們看到shared.write(new_data)
確實發生在shared.read()
之前。
案例二
現在假設相反,在m
的鎖定順序中,thr2 的鎖定在 thr1 的解鎖之前。 由於同一互斥鎖的鎖定和解鎖不能交錯(這是互斥鎖的全部要點,以提供互斥), m
上的鎖總順序必須如下:
thr2: m.lock()
thr2: m.unlock()
thr1: m.lock()
thr1: m.unlock()
這意味着 thr2 的m.unlock()
與 thr1 的m.lock()
()同步。 現在 updated.load( updated.load()
在 thr2 m.unlock()
之前排序,而 thr1 m.lock()
在 updated.store updated.store(true)
之前排序,因此 updated.load updated.load()
發生在updated.store(true)
之前. 通過讀寫一致性 [intro.races p17],updated.load updated.load()
不得從updated.store(true)
中獲取其值,而是從updated
的修改順序中的一些嚴格較早的副作用中獲取; 大概是它的初始化為false
。
我們得出結論,在這種情況下updated.load()
必須返回false
。 但如果真是這樣,那么 thr2 將永遠不會首先嘗試鎖定互斥量。 這是一個矛盾,所以情況 II 絕不會發生。
寬松的順序意味着原子和外部操作的排序只發生在特定原子 object 上的操作(即使這樣,編譯器也可以在程序定義的順序之外自由地重新排序它們)。 因此,輕松存儲與外部對象中的任何 state 都沒有關系。 因此,寬松的負載不會與您的其他互斥體同步。
獲取/釋放語義的全部要點是允許原子控制其他 memory 的可見性。如果您希望原子加載表示某物可用,則它必須是獲取並且它獲取的值必須已釋放。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.