无锁数据结构二-乱序控制(栅栏)
內(nèi)存柵欄
由于優(yōu)化會(huì)導(dǎo)致對(duì)代碼的亂序執(zhí)行,在并發(fā)執(zhí)行時(shí)可能帶來問題。因此為了并行代碼的正確執(zhí)行,我們需提示處理器對(duì)代碼優(yōu)化做一些限制。而這些提示就是內(nèi)存柵欄(memory barriers),用來對(duì)內(nèi)存訪問進(jìn)行管理。要詳細(xì)了解內(nèi)存柵欄原理及產(chǎn)生原因,可參考無鎖數(shù)據(jù)結(jié)構(gòu)(基礎(chǔ)篇):內(nèi)存柵障。每種處理器架構(gòu)都能提供一組完整的內(nèi)存柵欄供開發(fā)使用,使用這些,我們能建立不同的內(nèi)存模型。通過內(nèi)存模型,我們能控制并發(fā)的執(zhí)行順序,也即同步。
下面先對(duì)亂序進(jìn)行一些介紹,可參考3,4
亂序
亂序分為編譯器亂序和處理器(cpu)亂序,下面是我對(duì)它們的一些了解。
編譯器亂序
處理器亂序
由于亂序可在不同層進(jìn)行,柵欄也有多種,可分為編譯器內(nèi)存柵欄(編譯器),內(nèi)存柵欄(cpu)。對(duì)于c++11 來說其定義內(nèi)存模型,std::memory_order對(duì)編譯器和cpu都會(huì)施加影響。下面主要介紹c++11的內(nèi)存模型。
內(nèi)存模型與執(zhí)行順序
有如下三種內(nèi)存模型:
可分成四種執(zhí)行順序
所有這些內(nèi)存模型定義在一個(gè)C++列表中– std::memory_order,包含以下六個(gè)常量:
- memory_order_seq_cst 指向序列一致性模型
- memory_order_acquire, memory_order_release,memory_order_acq_rel, memory_order_consume 指向基于獲取/釋放語義的模型
- memory_order_relaxed 指向?qū)捤傻膬?nèi)存序列化模型
其中:
- memory_order_relaxed 只保證此操作是原子的,不保證任何讀寫內(nèi)存順序。
- memory_order_consume 對(duì)于當(dāng)前線程依賴于當(dāng)前原子變量A的變量的讀或?qū)懖荒苤嘏诺酱宋恢们?#xff0c;其他線程對(duì)依賴于A的變量的在釋放A前(如store(memory_order_release))的讀寫,對(duì)于當(dāng)前線程都是可見的(釋放/消費(fèi)語義)。在大多數(shù)平臺(tái)上,這只影響到編譯器優(yōu)化> 此語義作為一個(gè)“內(nèi)存的禮物”被引入DECAlpha處理器中。
- memory_order_acquire 帶此內(nèi)存順序的加載操作,在其影響的內(nèi)存位置進(jìn)行獲得操作:當(dāng)前線程中讀或?qū)懖荒苣鼙恢嘏诺酱思虞d前。其他釋放同一原子變量的線程的所有寫入,為當(dāng)前線程所可見(釋放/獲取語義)。與memory_order_consume區(qū)別:此標(biāo)志影響所有變量讀寫,而consume影響依賴原子變量的讀寫。
- memory_order_release 帶此內(nèi)存順序的存儲(chǔ)操作進(jìn)行釋放操作:當(dāng)前進(jìn)程中的讀或?qū)懖荒鼙恢嘏诺酱舜鎯?chǔ)后。當(dāng)前線程的所有寫入,可見于獲得(acquire)該同一原子變量的其他線程(釋放/獲取語義),并且對(duì)該原子變量的帶依賴寫入變得對(duì)于其他消費(fèi)同一原子對(duì)象的線程可見(釋放/消費(fèi)語義)。
- memory_order_acq_rel 帶此內(nèi)存順序的讀-修改-寫操作既是獲得操作又是釋放操作。當(dāng)前線程的讀或?qū)憙?nèi)存不能被重排到此存儲(chǔ)前或后。所有釋放同一原子變量的線程的寫入可見于修改之前,而且修改可見于其他獲得同一原子變量的線程。
- memory_order_seq_cst 任何帶此內(nèi)存順序的操作既是獲得操作又是釋放操作,加上存在一個(gè)單獨(dú)全序,其中所有線程以同一順序觀測(cè)到所有修改(序列一致)。
針對(duì)讀(加載),可選memory_order_acquire和 memory_order_consume。針對(duì)寫(存儲(chǔ)),僅能選memory_order_release。Memory_order_acq_rel是唯一可以用來做RMW運(yùn)算,比如compare_exchange, exchange, fetch_xxx。事實(shí)上,因?yàn)镽MW可以并發(fā)執(zhí)行原子讀\寫,原子性RMW原語擁有獲取語義memory_order_acquire, 釋放語義memory_order_release 或者 memory_order_acq_rel.?
-memory_order_acq_rel – is somehow similar to memory_order_seq_cst, but RMW-operation is located inside the acquire/release-section -memory_order_relaxed – RMW-operation shifting (its load and store parts) upwards/downwards the code (for example, within the acquire/release section, if the operation is located inside such section) doesn’t lead to errors
c++11可通過下列語句組合實(shí)現(xiàn)內(nèi)存模型
std::atomic::load syd::atomic::store ... ... std::atomic_thread_fence- 1
- 2
- 3
- 4
- 5
?
一般情況下上圖左右(store(memory_order_release)與atomic_thread_fence(memory_order_release))可以互相替換,但其仍有區(qū)別,Release Fence更加嚴(yán)格,其阻止下方的store穿過它,而Release Operation不行,如下,m_instance的store可在g_dummy.store上:
- 1
- 2
- 3
為簡單,在介紹模型時(shí)只介紹load/store
下面是我對(duì)各模型的理解
序列一致順序
這是一種嚴(yán)格的內(nèi)存模型,它確保處理器按程序本身既定順序執(zhí)行。
帶標(biāo)簽 memory_order_seq_cst 的原子操作不僅以與釋放/獲得順序相同的方式排序內(nèi)存(在一個(gè)線程中發(fā)生先于存儲(chǔ)的任何結(jié)果都變成做加載的線程中的可見副效應(yīng)),還對(duì)所有擁有此標(biāo)簽的內(nèi)存操作建立一個(gè)單獨(dú)全序。
釋放獲取順序
同步僅建立在釋放和獲得同一原子對(duì)象的線程之間。其他線程可能看到與被同步線程的一者或兩者相異的內(nèi)存訪問順序。?
在強(qiáng)順序系統(tǒng)( x86 、 SPARC TSO 、 IBM 主框架)上,釋放獲得順序?qū)τ诙鄶?shù)操作是自動(dòng)進(jìn)行的。無需為此同步模式添加額外的 CPU 指令,只有某些編譯器優(yōu)化受影響(例如,編譯器被禁止將非原子存儲(chǔ)移到原子存儲(chǔ)-釋放后,或?qū)⒎窃蛹虞d移到原子加載-獲得前)。在弱順序系統(tǒng)( ARM 、 Itanium 、 Power PC )上,必須使用特別的 CPU 加載或內(nèi)存柵欄指令。
通過下面代碼來說明此模型:
#include <thread> #include <atomic> #include <cassert> #include <vector>std::vector<int> data; std::atomic<int> flag = {0};void thread_1() {data.push_back(42);flag.store(1, std::memory_order_release);//memory_order_release 保證data.push_back(42)執(zhí)行順序一定在store前 }void thread_2() {int expected=1;while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) { //memory_order_acq_rel具有release和acquire,int expected=1;在前,expected = 1;在后//只有當(dāng)thread_1的store執(zhí)行后才會(huì)進(jìn)入循環(huán),此時(shí)能保證data==42,同時(shí)memory_order_acq_relexpected = 1; } }void thread_3() {while (flag.load(std::memory_order_acquire) < 2);//只有在執(zhí)行了flag.compare_exchange_strong后,才跳出循環(huán),此時(shí)已保證data==42assert(data.at(0) == 42); // 決不出錯(cuò) }int main() {std::thread a(thread_1);std::thread b(thread_2);std::thread c(thread_3);a.join(); b.join(); c.join(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
釋放消費(fèi)順序
使用memory_order_consume替換memory_order_acquire來提供比memory_order_acquire更弱的控制。
同步僅在釋放和消費(fèi)同一原子對(duì)象的線程間建立。其他線程能見到與被同步線程的一者或兩者相異的內(nèi)存訪問順序。
所有異于 DEC Alphi 的主流 CPU 上,依賴順序是自動(dòng)的,無需為此同步模式產(chǎn)生附加的 CPU 指令,只有某些編譯器優(yōu)化收益受影響(例如,編譯器被禁止?fàn)可娴揭蕾囨湹膶?duì)象上的推測(cè)性加載)。
注意當(dāng)前(2015年2月)沒有產(chǎn)品編譯器跟蹤依賴鏈:消費(fèi)操作被提升成獲得操作。
釋放消費(fèi)順序的規(guī)范正在修訂中,而且暫時(shí)不鼓勵(lì)使用 memory_order_consume (C++17 起)
例子如下:
#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_consume)));assert(*p2 == "Hello"); // 絕無出錯(cuò): *p2 從 ptr 攜帶依賴assert(data == 42); // 可能也可能不會(huì)出錯(cuò): data 不從 ptr 攜帶依賴,可能在ptr.load前執(zhí)行。 }int main() {std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
此順序的典型使用情況,涉及對(duì)很少被寫入的數(shù)據(jù)結(jié)構(gòu)(安排表、配置、安全策略、防火墻規(guī)則等)的共時(shí)讀取,和有指針中介發(fā)布的發(fā)布者-訂閱者情形,即當(dāng)生產(chǎn)者發(fā)布消費(fèi)者能通過其訪問信息的指針之時(shí):無需令生產(chǎn)者寫入內(nèi)存的所有其他內(nèi)容對(duì)消費(fèi)者可見(這在弱順序架構(gòu)上可能是昂貴的操作)。這種場(chǎng)景的一個(gè)例子是 rcu 解引用.
寬松順序
提供最弱控制,只保證原子性
典型使用是計(jì)數(shù)器自增,例如std::shared_ptr 的引用計(jì)數(shù)器,因?yàn)檫@只要求原子性,但不要求順序或同步
參考文檔
總結(jié)
以上是生活随笔為你收集整理的无锁数据结构二-乱序控制(栅栏)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 无锁数据结构一
- 下一篇: selenium操作chrome时的一些