這篇文章主要講解了“C++11中的原子量和內(nèi)存序有什么用”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“C++11中的原子量和內(nèi)存序有什么用”吧!
創(chuàng)新互聯(lián)專注于雙鴨山網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供雙鴨山營(yíng)銷型網(wǎng)站建設(shè),雙鴨山網(wǎng)站制作、雙鴨山網(wǎng)頁(yè)設(shè)計(jì)、雙鴨山網(wǎng)站官網(wǎng)定制、小程序設(shè)計(jì)服務(wù),打造雙鴨山網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供雙鴨山網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。一、多線程下共享變量的問題
(a) i++問題
在多線程編程中,最常拿來舉例的問題便是著名的i++ 問題,即:多個(gè)線程對(duì)同一個(gè)共享變量i執(zhí)行i++ 操作。這樣做之所以會(huì)出現(xiàn)問題的原因在于i++這個(gè)操作可以分為三個(gè)步驟:
step | operation |
---|---|
1 | i->reg(讀取i的值到寄存器) |
2 | inc-reg(在寄存器中自增i的值) |
3 | reg->i (寫回內(nèi)存中的i) |
上面三個(gè)步驟中間是可以間隔的,并非原子操作,也就是說多個(gè)線程同時(shí)執(zhí)行的時(shí)候可能出步驟的交叉執(zhí)行,例如下面的情況:
step | thread A | thread B |
---|---|---|
1 | i->reg | |
2 | inc-reg | |
3 | i->reg | |
4 | inc-reg | |
5 | reg->i | |
6 | reg->i |
假設(shè)i一開始為0,則執(zhí)行完第4步后,在兩個(gè)線程都認(rèn)為寄存器中的值為1,然后在第5、6兩步分別寫回去。最終兩個(gè)線程執(zhí)行完成后i的值為1。但是實(shí)際上我們?cè)趦蓚€(gè)線程中執(zhí)行了i++,原本希望i的值為2。i++ 實(shí)際上可以代表多線程編程中由于操作不是原子的而引發(fā)的交叉執(zhí)行這一類的問題,但是在這里我們先只關(guān)注對(duì)單個(gè)變量的操作。
(b)指令重排問題
有時(shí)候,我們會(huì)用一個(gè)變量作為標(biāo)志位,當(dāng)這個(gè)變量等于某個(gè)特定值的時(shí)候就進(jìn)行某些操作。但是這樣依然可能會(huì)有一些意想不到的坑,例如兩個(gè)線程以如下順序執(zhí)行:
step | thread A | thread B |
---|---|---|
1 | a = 1 | |
2 | flag= true | |
3 | if flag== true | |
4 | assert(a == 1) |
當(dāng)B判斷flag為true后,斷言a為1,看起來的確是這樣。那么一定是這樣嗎?可能不是,因?yàn)榫幾g器和CPU都可能將指令進(jìn)行重排(編譯器不同等級(jí)的優(yōu)化和CPU的亂序執(zhí)行)。實(shí)際上的執(zhí)行順序可能變成這樣:
step | thread A | thread B |
---|---|---|
1 | flag = true | |
2 | if flag== true | |
3 | assert(a == 1) | |
4 | a = 1 |
這種重排有可能會(huì)導(dǎo)致一個(gè)線程內(nèi)相互之間不存在依賴關(guān)系的指令交換執(zhí)行順序,以獲得更高的執(zhí)行效率。比如上面:flag 與 a 在A線程看起來是沒有任何依賴關(guān)系,似乎執(zhí)行順序無(wú)關(guān)緊要。但問題在于B使用了flag作為是否讀取a的依據(jù),A的指令重排可能會(huì)導(dǎo)致step3的時(shí)候斷言失敗。
解決方案
一個(gè)比較穩(wěn)妥的辦法就是對(duì)于共享變量的訪問進(jìn)行加鎖,加鎖可以保證對(duì)臨界區(qū)的互斥訪問,例如第一種場(chǎng)景如果加鎖后再執(zhí)行i++ 然后解鎖,則同一時(shí)刻只會(huì)有一個(gè)線程在執(zhí)行i++ 操作。另外,加鎖的內(nèi)存語(yǔ)義能保證一個(gè)線程在釋放鎖前的寫入操作一定能被之后加鎖的線程所見(即有happens before 語(yǔ)義),可以避免第二種場(chǎng)景中讀取到錯(cuò)誤的值。
那么如果覺得加鎖操作過重太麻煩而不想加鎖呢?C++11提供了一些原子變量與原子操作來支持。
二、 C++11的原子量
C++11標(biāo)準(zhǔn)在標(biāo)準(zhǔn)庫(kù)atomic頭文件提供了模版atomic<>來定義原子量:
template< class T > struct atomic;
它提供了一系列的成員函數(shù)用于實(shí)現(xiàn)對(duì)變量的原子操作,例如讀操作load,寫操作store,以及CAS操作compare_exchange_weak/compare_exchange_strong等。而對(duì)于大部分內(nèi)建類型,C++11提供了一些特化:
std::atomic_bool std::atomic<bool> std::atomic_char std::atomic<char> std::atomic_schar std::atomic<signed char> std::atomic_uchar std::atomic<unsigned char> std::atomic_short std::atomic<short> std::atomic_ushort std::atomic<unsigned short> std::atomic_int std::atomic<int> std::atomic_uint std::atomic<unsigned int> std::atomic_long std::atomic<long> ······ //更多類型見:http://en.cppreference.com/w/cpp/atomic/atomic
實(shí)際上這些特化就是相當(dāng)于取了一個(gè)別名,本質(zhì)上是同樣的定義。而對(duì)于整形的特化而言,會(huì)有一些特殊的成員函數(shù),例如原子加fetch_add、原子減fetch_sub、原子與fetch_and、原子或fetch_or等。常見操作符++、--、+=、&= 等也有對(duì)應(yīng)的重載版本。
接下來以int類型為例,解決我們的前面提到的i++ 場(chǎng)景中的問題。先定義一個(gè)int類型的原子量:
std::atomic<int> i;
由于int型的原子量重載了++ 操作符,所以i++ 是一個(gè)不可分割的原子操作,我們用多個(gè)線程執(zhí)行i++ 操作來進(jìn)行驗(yàn)證,測(cè)試代碼如下:
#include <iostream> #include <atomic> #include <vector> #include <functional> #include <thread> std::atomic<int> i; const int count = 100000; const int n = 10; void add() { for (int j = 0; j < count; ++j) i++; } int main() { i.store(0); std::vector<std::thread> workers; std::cout << "start " << n << " workers, " << "every woker inc " << count << " times" << std::endl; for (int j = 0; j < n; ++j) workers.push_back(std::move(std::thread(add))); for (auto & w : workers) w.join(); std::cout << "workers end " << "finally i is " << i << std::endl; if (i == n * count) std::cout << "i++ test passed!" << std::endl; else std::cout << "i++ test failed!" << std::endl; return 0; }
在測(cè)試中,我們定義了一個(gè)原子量i,在main函數(shù)開始的時(shí)候初始化為0,然后啟動(dòng)10個(gè)線程,每個(gè)線程執(zhí)行i++操作十萬(wàn)次,最終檢查i的值是否正確。執(zhí)行的最后結(jié)果如下:
start 10 workers, every woker inc 100000 times workers end finally i is 1000000 i++ test passed!
上面我們可以看到,10個(gè)線程同時(shí)進(jìn)行大量的自增操作,i的值依然正常。假如我們把i修改為一個(gè)普通的int變量,再次執(zhí)行程序可以得到結(jié)果如下:
start 10 workers, every woker inc 100000 times workers end finally i is 445227 i++ test failed!
顯然,由于自增操作各個(gè)步驟的交叉執(zhí)行,導(dǎo)致最后我們得到一個(gè)錯(cuò)誤的結(jié)果。
原子量可以解決i++問題,那么可以解決指令重排的問題嗎?也是可以的,和原子量選擇的內(nèi)存序有關(guān),我們把這個(gè)問題放到下一節(jié)專門研究。
上面已經(jīng)看到atomic是一個(gè)模版,那么也就意味著我們可以把自定義類型變成原子變量。但是是否任意類型都可以定義為原子類型呢?當(dāng)然不是,cppreference中的描述是必須為TriviallyCopyable類型。這個(gè)連接為TriviallyCopyable的詳細(xì)定義:
http://en.cppreference.com/w/cpp/concept/TriviallyCopyable
一個(gè)比較簡(jiǎn)單的判斷標(biāo)準(zhǔn)就是這個(gè)類型可以用std::memcpy按位復(fù)制,例如下面的類:
class { int x; int y; }
這個(gè)類是一個(gè)TriviallyCopyable類型,然而如果給它加上一個(gè)虛函數(shù):
class { int x; int y; virtual int add () { return x + y; } }
這個(gè)類便不能按位拷貝了,不滿足條件,不能進(jìn)行原子化。
如果一個(gè)類型能夠滿足atomic模版的要求,可以原子化,它就不用進(jìn)行加鎖操作了,因而速度更快嗎?依然不是,atomic有一個(gè)成員函數(shù)is_lock_free,這個(gè)成員函數(shù)可以告訴我們到底這個(gè)類型的原子量是使用了原子CPU指令實(shí)現(xiàn)了無(wú)鎖化,還是依然使用的加鎖的方式來實(shí)現(xiàn)原子操作。不過不管是否用鎖來實(shí)現(xiàn),atomic的使用方式和表現(xiàn)出的語(yǔ)義都是沒有區(qū)別的。具體用哪種方式實(shí)現(xiàn)C++標(biāo)準(zhǔn)并沒有做約束(除了std::atomic_flag特化要求必須為lock free),跟平臺(tái)有關(guān)。
例如在我的Cygwin64、GCC7.3環(huán)境下執(zhí)行如下代碼:
#include <iostream> #include <atomic> #define N 8 struct A { char a[N]; }; int main() { std::atomic<A> a; std::cout << sizeof(A) << std::endl; std::cout << a.is_lock_free() << std::endl; return 0; }
結(jié)果為:
8
1
證明上面定義的類型A的原子量是無(wú)鎖的。我在這個(gè)平臺(tái)上進(jìn)行了實(shí)驗(yàn),修改N的大小,結(jié)果如下:
N | sizeof(A) | is_lock_free() |
---|---|---|
1 | 1 | 1 |
2 | 2 | 1 |
3 | 3 | 0 |
4 | 4 | 1 |
5 | 5 | 0 |
6 | 6 | 0 |
7 | 7 | 0 |
8 | 8 | 1 |
> 8 | / | 0 |
將A修改為內(nèi)建類型,對(duì)于內(nèi)建類型的實(shí)驗(yàn)結(jié)果如下:
type | sizeof() | is_lock_free() |
---|---|---|
char | 1 | 1 |
short | 2 | 1 |
int | 4 | 1 |
long long | 8 | 1 |
float | 4 | 1 |
double | 8 | 1 |
可以看出在我的平臺(tái)下常用內(nèi)建類型都是lock free的,自定義類型則和大小有關(guān)。
從上面的統(tǒng)計(jì)還可以看出似乎當(dāng)自定義類型的長(zhǎng)度和某種自定義類型相等的時(shí)候is_lock_free()就為true。我推測(cè)可能我這里的atomic實(shí)現(xiàn)的無(wú)鎖是通過編譯器內(nèi)建的原子操作實(shí)現(xiàn)的,只有當(dāng)數(shù)據(jù)長(zhǎng)度剛好能調(diào)用編譯器內(nèi)建原子操作時(shí)才能進(jìn)行無(wú)鎖化。查看GCC參考手冊(cè)(/tupian/20230522/_005f_005fatomic-Builtins.html 中內(nèi)建原子操作的原型,以CAS操作為例:
bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder) bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder)
其參數(shù)類型為type *的指針,在同一頁(yè)可以找到GCC關(guān)于type的描述:
The ‘__atomic' builtins can be used with any integral scalar or pointer type that is 1, 2, 4, or 8 bytes in length. 16-byte integral types are also allowed if ‘__int128' (see __int128) is supported by the architecture.
type類型的長(zhǎng)度應(yīng)該為1、2、4、8字節(jié)中的一個(gè),少數(shù)支持__int128的平臺(tái)可以到16字節(jié),所以只有長(zhǎng)度為1,2,4,8字節(jié)的數(shù)據(jù)才能實(shí)現(xiàn)無(wú)鎖。這個(gè)只是我的推測(cè),具體是否如此尚不明白。
三、C++11的六種內(nèi)存序
前面我們解決i++問題的時(shí)候已經(jīng)使用過原子量的寫操作load將原子量賦值,實(shí)際上成員函數(shù)還有另一個(gè)參數(shù):
void store( T desired, std::memory_order order = std::memory_order_seq_cst )
這個(gè)參數(shù)代表了該操作使用的內(nèi)存序,用于控制變量在不同線程見的順序可見性問題,不只load,其他成員函數(shù)也帶有該參數(shù)。c++11提供了六種內(nèi)存序供選擇,分別為:
typedef enum memory_order { memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst } memory_order;
之前在場(chǎng)景2中,因?yàn)橹噶畹闹嘏艑?dǎo)致了意料之外的錯(cuò)誤,通過使用原子變量并選擇合適內(nèi)存序,可以解決這個(gè)問題。下面先來看看這幾種內(nèi)存序
memory_order_release/memory_order_acquire
內(nèi)存序選項(xiàng)用來作為原子量成員函數(shù)的參數(shù),memory_order_release用于store操作,memory_order_acquire用于load操作,這里我們把使用了memory_order_release的調(diào)用稱之為release操作。從邏輯上可以這樣理解:release操作可以阻止這個(gè)調(diào)用之前的讀寫操作被重排到后面去,而acquire操作則可以保證調(diào)用之后的讀寫操作不會(huì)重排到前面來。聽起來有種很繞的感覺,還是以一個(gè)例子來解釋:假設(shè)flag為一個(gè) atomic特化的bool 原子量,a為一個(gè)int變量,并且有如下時(shí)序的操作:
step | thread A | thread B |
---|---|---|
1 | a = 1 | |
2 | flag.store(true, memory_order_release) | |
3 | if( true == flag.load(memory_order_acquire)) | |
4 | assert(a == 1) |
實(shí)際上這就是把我們上文場(chǎng)景2中的flag變量換成了原子量,并用其成員函數(shù)進(jìn)行讀寫。在這種情況下的邏輯順序上,step1不會(huì)跑到step2后面去,step4不會(huì)跑到step3前面去。這樣一來,實(shí)際上我們就已經(jīng)保證了當(dāng)讀取到flag為true的時(shí)候a一定已經(jīng)被寫入為1了,場(chǎng)景2得到了解決。換一種比較嚴(yán)謹(jǐn)?shù)拿枋龇绞娇梢钥偨Y(jié)為:
對(duì)于同一個(gè)原子量,release操作前的寫入,一定對(duì)隨后acquire操作后的讀取可見。
這兩種內(nèi)存序是需要配對(duì)使用的,這也是將他們放在一起介紹的原因。還有一點(diǎn)需要注意的是:只有對(duì)同一個(gè)原子量進(jìn)行操作才會(huì)有上面的保證,比如step3如果是讀取了另一個(gè)原子量flag2,是不能保證讀取到a的值為1的。
memory_order_release/memory_order_consume
memory_order_release還可以和memory_order_consume搭配使用。memory_order_release操作的作用沒有變化,而memory_order_consume用于load操作,我們簡(jiǎn)稱為consume操作,comsume操作防止在其后對(duì)原子變量有依賴的操作被重排到前面去。這種情況下:
對(duì)于同一個(gè)原子變量,release操作所依賴的寫入,一定對(duì)隨后consume操作后依賴于該原子變量的操作可見。
這個(gè)組合比上一種更寬松,comsume只阻止對(duì)這個(gè)原子量有依賴的操作重拍到前面去,而非像aquire一樣全部阻止。將上面的例子稍加改造來展示這種內(nèi)存序,假設(shè)flag為一個(gè) atomic特化的bool 原子量,a為一個(gè)int變量,b、c各為一個(gè)bool變量,并且有如下時(shí)序的操作:
step | thread A | thread B |
---|---|---|
1 | b = true | |
2 | a = 1 | |
3 | flag.store(b, memory_order_release) | |
4 | while (!(c = flag.load(memory_order_consume))) | |
5 | assert(a == 1) | |
6 | assert(c == true) | |
7 | assert(b == true) |
step4使得c依賴于flag,當(dāng)step4線程B讀取到flag的值為true的時(shí)候,由于flag依賴于b,b在之前的寫入是可見的,此時(shí)b一定為true,所以step6、step7的斷言一定會(huì)成功。而且這種依賴關(guān)系具有傳遞性,假如b又依賴與另一個(gè)變量d,則d在之前的寫入同樣對(duì)step4之后的操作可見。那么a呢?很遺憾在這種內(nèi)存序下a并不能得到保證,step5的斷言可能會(huì)失敗。
memory_order_acq_rel
這個(gè)選項(xiàng)看名字就很像release和acquire的結(jié)合體,實(shí)際上它的確兼具兩者的特性。這個(gè)操作用于“讀取-修改-寫回”這一類既有讀取又有修改的操作,例如CAS操作。可以將這個(gè)操作在內(nèi)存序中的作用想象為將release操作和acquire操作捆在一起,因此任何讀寫操作的重拍都不能跨越這個(gè)調(diào)用。依然以一個(gè)例子來說明,flag為一個(gè) atomic特化的bool 原子量,a、c各為一個(gè)int變量,b為一個(gè)bool變量,并且剛好按如下順序執(zhí)行:
step | thread A | thread B |
---|---|---|
1 | a = 1 | |
2 | flag.store(true, memory_order_release) | |
3 | b = true | |
4 | c = 2 | |
5 | while (!flag.compare_exchange_weak(b, false, memory_order_acq_rel)) {b = true} | |
6 | assert(a == 1) | |
7 | if (true == flag.load(memory_order_acquire) | |
8 | assert(c == 2) |
由于memory_order_acq_rel同時(shí)具有memory_order_release與memory_order_acquire的作用,因此step2可以和step5組合成上面提到的release/acquire組合,因此step6的斷言一定會(huì)成功,而step5又可以和step7組成release/acquire組合,step8的斷言同樣一定會(huì)成功。
memory_order_seq_cst
這個(gè)內(nèi)存序是各個(gè)成員函數(shù)的內(nèi)存序默認(rèn)選項(xiàng),如果不選擇內(nèi)存序則默認(rèn)使用memory_order_seq_cst。這是一個(gè)“美好”的選項(xiàng),如果對(duì)原子變量的操作都是使用的memory_order_seq_cst內(nèi)存序,則多線程行為相當(dāng)于是這些操作都以一種特定順序被一個(gè)線程執(zhí)行,在哪個(gè)線程觀察到的對(duì)這些原子量的操作都一樣。同時(shí),任何使用該選項(xiàng)的寫操作都相當(dāng)于release操作,任何讀操作都相當(dāng)于acquire操作,任何“讀取-修改-寫回”這一類的操作都相當(dāng)于使用memory_order_acq_rel的操作。
memory_order_relaxed
這個(gè)選項(xiàng)如同其名字,比較松散,它僅僅只保證其成員函數(shù)操作本身是原子不可分割的,但是對(duì)于順序性不做任何保證。
代價(jià)
總的來講,越嚴(yán)格的內(nèi)存序其性能開銷會(huì)越大。對(duì)于我們常用的x86處理器而言,在處理器層級(jí)本身就支持release/acquire語(yǔ)義,因此release與acquire/consume都只影響編譯器的優(yōu)化,而memory_order_seq_cst還會(huì)影響處理器的指令重排。
感謝各位的閱讀,以上就是“C++11中的原子量和內(nèi)存序有什么用”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)C++11中的原子量和內(nèi)存序有什么用這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián)網(wǎng)站建設(shè)公司,,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
本文標(biāo)題:C++11中的原子量和內(nèi)存序有什么用-創(chuàng)新互聯(lián)
分享鏈接:http://bm7419.com/article32/dscssc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供虛擬主機(jī)、定制網(wǎng)站、網(wǎng)站營(yíng)銷、網(wǎng)站改版、網(wǎng)站建設(shè)、定制開發(fā)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容