鏈接:https://taozj.org/2016/09/C-11開發(fā)中的Atomic原子操作/
原子操作在多線程開發(fā)中經(jīng)常用到,比如在計數(shù)器,序列產(chǎn)生器等地方,這類情況下數(shù)據(jù)有并發(fā)的危險,但是用鎖去保護又顯得有些浪費,所以原子類型操作十分的方便。
原子操作雖然用起來簡單,但是其背景遠比我們想象的要復(fù)雜。其主要在于現(xiàn)代計算系統(tǒng)過于的復(fù)雜:多處理器、多核處理器、處理器又有核心獨有以及核心共享的多級緩存,在這種情況下,一個核心修改了某個變量,其他核心什么時候可見是一個十分嚴肅的問題。同時在極致追求性能的時代,處理器和編譯器往往表現(xiàn)的很智能,進行極度的優(yōu)化,比如什么亂序執(zhí)行、指令重排等,雖然可以在當(dāng)前上下文中做到很好的優(yōu)化,但是放在多核環(huán)境下常常會引出新的問題來,這時候就必須提示編譯器和處理器某種提示,告訴某些代碼的執(zhí)行順序不能被優(yōu)化。
所以這里說到的原子操作,基本都包含我們?nèi)齻€方面所關(guān)心的語義:操作本身是不可分割的(Atomicity),一個線程對某個數(shù)據(jù)的操作何時對另外一個線程可見(Visibility),執(zhí)行的順序是否可以被重排(Ordering)。
據(jù)說在C++11標(biāo)準(zhǔn)出來之前,大家都詬病C++標(biāo)準(zhǔn)沒有一個明確的內(nèi)存模型,隨著多線程開發(fā)的普及這個問題顯得越來越迫切。當(dāng)然各個C++編譯器實現(xiàn)者也是各自為政,GCC自然是實用主義當(dāng)?shù)溃谑歉鶕?jù)Intel的開發(fā)手冊老早就搞出了一系列的__sync原子操作函數(shù)集合,這也是被廣大程序員最為熟悉常用的操作了吧,羅列如下:type?__sync_fetch_and_OP?(type?*ptr,?type?value,?...)
type?__sync_OP_and_fetch?(type?*ptr,?type?value,?...)
bool?__sync_bool_compare_and_swap?(type?*ptr,?type?oldval,?type?newval,?...)
type?__sync_val_compare_and_swap?(type?*ptr,?type?oldval,?type?newval,?...)
__sync_synchronize?(...)
type?__sync_lock_test_and_set?(type?*ptr,?type?value,?...)
void?__sync_lock_release?(type?*ptr,?...)
上面的OP操作包括add、sub、or、and、xor、nand這些常見的數(shù)學(xué)操作,而type表示的數(shù)據(jù)類型Intel官方允許的是int、long、long long的帶符號和無符號類型,但是GCC擴展后允許任意1/2/4/8的標(biāo)量類型;CAS的操作有兩個版本分別返回bool表示是否成功,而另外一個在操作之前會先返回ptr地址處存儲的值;__sync_synchronize直接插入一個full memory barrier,當(dāng)然你也可能經(jīng)常見到像asm volatile(“” ::: “memory”);這樣的操作。前面的這些原子操作都是full barrier類型的,這意味著:任何內(nèi)存操作的指令不允許跨越這些操作重新排序。__sync_lock_test_and_set用于將value的值寫入ptr的位置,同時返回ptr之前存儲的值,其內(nèi)存模型是acquire barrier,意味著該操作之后的memory store指令不允許重排到該操作之前去,不過該操作之前的memory store可以排到該操作之后去,而__sync_lock_release則更像是對前面一個操作鎖的釋放,通常意味著將0寫入ptr的位置,該操作是release barrier,意味著之前的memory store是全局可見的,所有的memory load也都完成了,但是接下來的內(nèi)存讀取可能會被排序到該操作之前執(zhí)行??梢赃@里比較繞,翻譯起來也比較的拗口,不過據(jù)我所見,這里很多是用在自旋鎖類似的操作上,比如:static?volatile?int?_sync;
static?void?lock_sync()?{
while(__sync_lock_test_and_set(&_sync,?1));
}
static?void?unlock_sync()?{
__sync_lock_release(&_sync);
}
其實這里的1可以是任何non-zero的值,主要是用作bool的效果。二、C++11 新標(biāo)準(zhǔn)中的內(nèi)存模型
上面GCC那種full barrier的操作確實有效,但是就像當(dāng)初系統(tǒng)內(nèi)核從單核切換到多核用大顆粒鎖一樣的簡單粗暴,先不說這種形勢下編譯器和處理器無法進行優(yōu)化,光要變量使其對他處理器可見,就需要在處理間進行硬件級別的同步,顯然是十分耗費資源的。在C++11新標(biāo)準(zhǔn)中規(guī)定的內(nèi)存模型(memory model)顆粒要細化的多,如果熟悉這些內(nèi)存模型,在保證業(yè)務(wù)正確的同時可以將對性能的影響減弱到最低。
原子變量的通用接口使用store()和load()方式進行存取,可以額外接受一個額外的memory order參數(shù),而不傳遞的話默認是最強模式Sequentially Consistent。
根據(jù)執(zhí)行線程之間對變量的同步需求強度,新標(biāo)準(zhǔn)下的內(nèi)存模型可以分成如下幾類:
2.1 Sequentially Consistent
該模型是最強的同步模式,參數(shù)表示為std::memory_order_seq_cst,同時也是默認的模型。-Thread?1-?-Thread?2-
y?=?1?if?(x.load()?==?2)
x.store?(2);?assert?(y?==?1)
對于上面的例子,即使x和y是不相關(guān)的,通常情況下處理器或者編譯器可能會對其訪問進行重排,但是在seq_cst模式下,x.store(2)之前的所有memory accesses都會happens-before在這次store操作。另外一個角度來說:對于seq_cst模式下的操作,所有memory accesses操作的重排不允許跨域這個操作,同時這個限制是雙向的。
GCC的wiki可能講的不太清楚,查看下面的典型Acquire/Release的使用例子:std::atomic<int>?a{0};
int?b?=?0;
-Thread?1-
b?=?1;
a.store(1,?memory_order_release);
-Thread?2-
while?(a.load(memory_order_acquire)?!=?1)??/*waiting*/;
std::cout?<'\n';
毫無疑問,如果是seq_cst,那么上面的操作一定是成功的(打印變量b顯示為1)。a. memory_order_release保證在這個操作之前的memory accesses不會重排到這個操作之后去,但是這個操作之后的memory accesses可能會重排到這個操作之前去。通常這個主要是用于之前準(zhǔn)備某些資源后,通過store+memory_order_release的方式”Release”給別的線程;b. memory_order_acquire保證在這個操作之后的memory accesses不會重排到這個操作之前去,但是這個操作之前的memory accesses可能會重排到這個操作之后去。通常通過load+memory_order_acquire判斷或者等待某個資源,一旦滿足某個條件后就可以安全的“Acquire”消費這些資源了。
這是一個相比Acquire/Release更加寬松的內(nèi)存模型,對非依賴的變量也去除了happens-before的限制,減少了所需同步的數(shù)據(jù)量,可以加快執(zhí)行的速度。-Thread?1-
n?=?1
m?=?1
p.store?(&n,?memory_order_release)
-Thread?2-
t?=?p.load?(memory_order_acquire);
assert(?*t?==?1?&&?m?==?1?);
-Thread?3-
t?=?p.load?(memory_order_consume);
assert(?*t?==?1?&&?m?==?1?);
線程2的assert會pass,而線程3的assert可能會fail,因為n出現(xiàn)在了store表達式中,算是一個依賴變量,會確保對該變量的memory access會happends-before在這個store之前,但是m沒有依賴關(guān)系,所以不會同步該變量,對其值不作保證。Comsume模式因為降低了需要在硬件之間同步的數(shù)量,所以理論上其執(zhí)行的速度會比之上面的內(nèi)存模型快一些,尤其在共享內(nèi)存大規(guī)模數(shù)據(jù)量情況下,應(yīng)該會有較明顯的差異表現(xiàn)出來。在這里,Acquire/Consume~Release這種線程間同步協(xié)作的機制就被完全暴露了,通常會形成Acquired/Consume來等待Release的某個狀態(tài)更新。需要注意的是這樣的通信需要兩個線程間成對的使用才有意義,同時對于沒有使用這個內(nèi)存模型的第三方線程沒有任何作用效果。
最寬松的模式,memory_order_relaxed沒有happens-before的約束,編譯器和處理器可以對memory access做任何的re-order,因此另外的線程不能對其做任何的假設(shè),這種模式下能做的唯一保證,就是一旦線程讀到了變量var的最新值,那么這個線程將再也見不到var修改之前的值了。這種情況通常是在需要原子變量,但是不在線程間同步共享數(shù)據(jù)的時候會用,同時當(dāng)relaxed存一個數(shù)據(jù)的時候,另外的線程將需要一個時間才能relaxed讀到該值,在非緩存一致性的構(gòu)架上需要刷新緩存。在開發(fā)的時候,如果你的上下文沒有共享的變量需要在線程間同步,選用Relaxed就可以了。
看到這里,你對Atomic原子操作,應(yīng)當(dāng)不僅僅停留在indivisable的層次了,因為所有的內(nèi)存模型都能保證對變量的修改是原子的,C++11新標(biāo)準(zhǔn)的原子應(yīng)該上升到了線程間數(shù)據(jù)同步和協(xié)作的問題了,跟前面的LockFree關(guān)系也比較密切。手冊上也這樣告誡菜鳥程序員:除非你知道這是什么,需要減弱線程間原子上下文同步的耦合性增加執(zhí)行效率,才考慮這里的內(nèi)存模型來優(yōu)化你的程序,否則還是老老實實的使用默認的memory_order_seq_cst,雖然速度可能會慢點,但是穩(wěn)妥些,萬一由于你不成熟的優(yōu)化帶來問題,是很難去調(diào)試的。
GCC實現(xiàn)了C++11之后,上面的__sync系列操作就變成了Legacy而不被推薦使用了,而基于C++11的新原子操作接口使用__atomic作為前綴。對于普通的數(shù)學(xué)操作函數(shù),其函數(shù)接口形式為:type?__atomic_OP_fetch?(type?*ptr,?type?val,?int?memorder);
type?__atomic_fetch_OP?(type?*ptr,?type?val,?int?memorder);
除此之外,還根據(jù)新標(biāo)準(zhǔn)提供了一些新的接口:type?__atomic_load_n?(type?*ptr,?int?memorder);
void?__atomic_store_n?(type?*ptr,?type?val,?int?memorder);
type?__atomic_exchange_n?(type?*ptr,?type?val,?int?memorder);
bool?__atomic_compare_exchange_n?(type?*ptr,?type?*expected,?type?desired,?bool?weak,?int?success_memorder,?int?failure_memorder);
bool?__atomic_test_and_set?(void?*ptr,?int?memorder);
void?__atomic_clear?(bool?*ptr,?int?memorder);
void?__atomic_thread_fence?(int?memorder);
bool?__atomic_always_lock_free?(size_t?size,?void?*ptr);
bool?__atomic_is_lock_free?(size_t?size,?void?*ptr);
從函數(shù)名,看起來意思也很明了吧,上面的帶_n的后綴版本如果去掉_n就是不用提供memorder的seq_cst版本。最后的兩個函數(shù),是判斷系統(tǒng)上對于某個長度的對象是否會產(chǎn)生lock-free的原子操作,一般long long這種8個字節(jié)是沒有問題的,對于支持128位整形的構(gòu)架就可以達到16字節(jié)無鎖結(jié)構(gòu)了。Boost.Asio這里就不在羅列了,不過其中有一些例子比較好,基于內(nèi)存模型的Wait-free的ring buffer、producer-customer的例子,可以去看看。版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認,都會標(biāo)明作者及出處,如有侵權(quán),煩請告知,我們會立即刪除并致歉!