C++11開發中的Atomic原子操作


C++11開發中的Atomic原子操作

原子操作在多線程開發中經常用到,比如在計數器,序列產生器等地方,這類情況下數據有並發的危險,但是用鎖去保護又顯得有些浪費,所以原子類型操作十分的方便。

原子操作雖然用起來簡單,但是其背景遠比我們想象的要復雜。其主要在於現代計算系統過於的復雜:多處理器、多核處理器、處理器又有核心獨有以及核心共享的多級緩存,在這種情況下,一個核心修改了某個變量,其他核心什么時候可見是一個十分嚴肅的問題。同時在極致最求性能的時代,處理器和編譯器往往表現的很智能,進行極度的優化,比如什么亂序執行、指令重排等,雖然可以在當前上下文中做到很好的優化,但是放在多核環境下常常會引出新的問題來,這時候就必須提示編譯器和處理器某種提示,告訴某些代碼的執行順序不能被優化。

所以這里說到的原子操作,基本都包含我們三個方面所關心的語義:操作本身是不可分割的(Atomicity),一個線程對某個數據的操作何時對另外一個線程可見(Visibility),執行的順序是否可以被重排(Ordering)。

一、legacy GCC __sync

據說在C++11標准出來之前,大家都詬病C++標准沒有一個明確的內存模型,隨着多線程開發的普及這個問題顯得越來越迫切。當然各個C++編譯器實現者也是各自為政,GCC自然是實用主義當道,於是根據Intel的開發手冊老早就搞出了一系列的__sync原子操作函數集合,這也是被廣大程序員最為熟悉常用的操作了吧,羅列如下:

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這些常見的數學操作,而type表示的數據類型Intel官方允許的是int、long、long long的帶符號和無符號類型,但是GCC擴展后允許任意1/2/4/8的標量類型;CAS的操作有兩個版本分別返回bool表示是否成功,而另外一個在操作之前會先返回ptr地址處存儲的值;__sync_synchronize直接插入一個full memory barrier,當然你也可能經常見到像asm volatile(“” ::: “memory”);這樣的操作。前面的這些原子操作都是full barrier類型的,這意味着:任何內存操作的指令不允許跨越這些操作重新排序。

__sync_lock_test_and_set用於將value的值寫入ptr的位置,同時返回ptr之前存儲的值,其內存模型是acquire barrier,意味着該操作之后的memory store指令不允許重排到該操作之前去,不過該操作之前的memory store可以排到該操作之后去,而__sync_lock_release則更像是對前面一個操作鎖的釋放,通常意味着將0寫入ptr的位置,該操作是release barrier,意味着之前的memory store是全局可見的,所有的memory load也都完成了,但是接下來的內存讀取可能會被排序到該操作之前執行。可以這里比較繞,翻譯起來也比較的拗口,不過據我所見,這里很多是用在自旋鎖類似的操作上,比如:

staticvolatileint_sync;

staticvoidlock_sync(){ while(__sync_lock_test_and_set(&_sync,1)); } staticvoidunlock_sync(){ __sync_lock_release(&_sync); } 

其實這里的1可以是任何non-zero的值,主要是用作bool的效果。

二、C++11 新標准中的內存模型

上面GCC那種full barrier的操作確實有效,但是就像當初系統內核從單核切換到多核用大顆粒鎖一樣的簡單粗暴,先不說這種形勢下編譯器和處理器無法進行優化,光要變量使其對他處理器可見,就需要在處理間進行硬件級別的同步,顯然是十分耗費資源的。在C++11新標准中規定的內存模型(memory model)顆粒要細化的多,如果熟悉這些內存模型,在保證業務正確的同時可以將對性能的影響減弱到最低。

原子變量的通用接口使用store()和load()方式進行存取,可以額外接受一個額外的memory order參數,而不傳遞的話默認是最強模式Sequentially Consistent。

根據執行線程之間對變量的同步需求強度,新標准下的內存模型可以分成如下幾類:

2.1 Sequentially Consistent

該模型是最強的同步模式,參數表示為std::memory_order_seq_cst,同時也是默認的模型。

-Thread 1- -Thread2-
y = 1if(x.load() ==2) x.store (2); assert (y ==1) 

對於上面的例子,即使x和y是不相關的,通常情況下處理器或者編譯器可能會對其訪問進行重排,但是在seq_cst模式下,x.store(2)之前的所有memory accesses都會happens-before在這次store操作。

另外一個角度來說:對於seq_cst模式下的操作,所有memory accesses操作的重排不允許跨域這個操作,同時這個限制是雙向的。

2.2 Acquire/Release

GCC的wiki可能講的不太清楚,查看下面的典型Acquire/Release的使用例子:

std::atomic<int> a{0}; intb =0; -Thread 1- b = 1; a.store(1, memory_order_release); -Thread 2- while(a.load(memory_order_acquire) !=1)/*waiting*/; std::cout<< b <<'\n'; 

毫無疑問,如果是seq_cst,那么上面的操作一定是成功的(打印變量b顯示為1)。

a. memory_order_release保證在這個操作之前的memory accesses不會重排到這個操作之后去,但是這個操作之后的memory accesses可能會重排到這個操作之前去。通常這個主要是用於之前准備某些資源后,通過store+memory_order_release的方式”Release”給別的線程;

b. memory_order_acquire保證在這個操作之后的memory accesses不會重排到這個操作之前去,但是這個操作之前的memory accesses可能會重排到這個操作之后去。通常通過load+memory_order_acquire判斷或者等待某個資源,一旦滿足某個條件后就可以安全的“Acquire”消費這些資源了。

2.3 Consume

這是一個相比Acquire/Release更加寬松的內存模型,對非依賴的變量也去除了happens-before的限制,減少了所需同步的數據量,可以加快執行的速度。

-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出現在了store表達式中,算是一個依賴變量,會確保對該變量的memory access會happends-before在這個store之前,但是m沒有依賴關系,所以不會同步該變量,對其值不作保證。

Comsume模式因為降低了需要在硬件之間同步的數量,所以理論上其執行的速度會比之上面的內存模型塊一些,尤其在共享內存大規模數據量情況下,應該會有較明顯的差異表現出來。

在這里,Acquire/Consume~Release這種線程間同步協作的機制就被完全暴露了,通常會形成Acquired/Consume來等待Release的某個狀態更新。需要注意的是這樣的通信需要兩個線程間成對的使用才有意義,同時對於沒有使用這個內存模型的第三方線程沒有任何作用效果。

2.4 Relaxed

最寬松的模式,memory_order_relaxed沒有happens-before的約束,編譯器和處理器可以對memory access做任何的re-order,因此另外的線程不能對其做任何的假設,這種模式下能做的唯一保證,就是一旦線程讀到了變量var的最新值,那么這個線程將再也見不到var修改之前的值了。

這種情況通常是在需要原子變量,但是不在線程間同步共享數據的時候會用,同時當relaxed存一個數據的時候,另外的線程將需要一個時間才能relaxed讀到該值,在非緩存一致性的構架上需要刷新緩存。在開發的時候,如果你的上下文沒有共享的變量需要在線程間同步,選用Relaxed就可以了。

2.5 小結

看到這里,你對Atomic原子操作,應當不僅僅停留在indivisable的層次了,因為所有的內存模型都能保證對變量的修改是原子的,C++11新標准的原子應該上升到了線程間數據同步和協作的問題了,跟前面的LockFree關系也比較密切。

手冊上也這樣告誡菜鳥程序員:除非你知道這是什么,需要減弱線程間原子上下文同步的耦合性增加執行效率,才考慮這里的內存模型來優化你的程序,否則還是老老實實的使用默認的memory_order_seq_cst,雖然速度可能會慢點,但是穩妥些,萬一由於你不成熟的優化帶來問題,是很難去調試的。

三、C++11 GCC __atomic

GCC實現了C++11之后,上面的__sync系列操作就變成了Legacy而不被推薦使用了,而基於C++11的新原子操作接口使用__atomic作為前綴。

對於普通的數學操作函數,其函數接口形式為:

type __atomic_OP_fetch (type *ptr, type val,intmemorder); type __atomic_fetch_OP (type *ptr, type val,intmemorder); 

除此之外,還根據新標准提供了一些新的接口:

type __atomic_load_n (type *ptr,intmemorder); void__atomic_store_n (type *ptr, type val,intmemorder); type __atomic_exchange_n (type *ptr, type val,intmemorder); bool__atomic_compare_exchange_n (type *ptr, type *expected, type desired,boolweak,intsuccess_memorder,intfailure_memorder); bool__atomic_test_and_set (void*ptr,intmemorder); void__atomic_clear (bool*ptr,intmemorder); void__atomic_thread_fence (intmemorder); bool__atomic_always_lock_free (size_tsize,void*ptr); bool__atomic_is_lock_free (size_tsize,void*ptr); 

從函數名,看起來意思也很明了吧,上面的帶_n的后綴版本如果去掉_n就是不用提供memorder的seq_cst版本。最后的兩個函數,是判斷系統上對於某個長度的對象是否會產生lock-free的原子操作,一般long long這種8個字節是沒有問題的,對於支持128位整形的構架就可以達到16字節無鎖結構了。

Boost.Asio這里就不在羅列了,不過其中有一些例子比較好,基於內存模型的Wait-free的ring buffer、producer-customer的例子,可以去看看。

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM