簡體   English   中英

與互斥量同步並放松 memory 順序原子

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM