Linux環(huán)境多線(xiàn)程編程基礎(chǔ)設(shè)施
本文介紹多線(xiàn)程環(huán)境下并行編程的基礎(chǔ)設(shè)施。主要包括:
- volatile
- __thread
- Memory Barrier
- __sync_synchronize
volatile
編譯器有時(shí)候?yàn)榱藘?yōu)化性能,會(huì)將一些變量的值緩存到寄存器中,因此如果編譯器發(fā)現(xiàn)該變量的值沒(méi)有改變的話(huà),將從寄存器里讀出該值,這樣可以避免內(nèi)存訪(fǎng)問(wèn)。
但是這種做法有時(shí)候會(huì)有問(wèn)題。如果該變量確實(shí)(以某種很難檢測(cè)的方式)被修改呢?那豈不是讀到錯(cuò)的值?是的。在多線(xiàn)程情況下,問(wèn)題更為突出:當(dāng)某個(gè)線(xiàn)程對(duì)一個(gè)內(nèi)存單元進(jìn)行修改后,其他線(xiàn)程如果從寄存器里讀取該變量可能讀到老值,未更新的值,錯(cuò)誤的值,不新鮮的值。
如何防止這樣錯(cuò)誤的“優(yōu)化”?方法就是給變量加上volatile修飾。
- volatile int i=10;//用volatile修飾變量i
- ......//something happened
- int b = i;//強(qiáng)制從內(nèi)存中讀取實(shí)時(shí)的i的值
OK,畢竟volatile不是完美的,它也在某種程度上限制了優(yōu)化。有時(shí)候是不是有這樣的需求:我要你立即實(shí)時(shí)讀取數(shù)據(jù)的時(shí)候,你就訪(fǎng)問(wèn)內(nèi)存,別優(yōu)化;否則,你該優(yōu)化還是優(yōu)化你的。能做到嗎?
不加volatile修飾,那么就做不到前面一點(diǎn)。加了volatile,后面這一方面就無(wú)從談起,怎么辦?傷腦筋。
其實(shí)我們可以這樣:
- int i = 2; //變量i還是不用加volatile修飾
- #define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x))
需要實(shí)時(shí)讀取i的值時(shí)候,就調(diào)用ACCESS_ONCE(i),否則直接使用i即可。
這個(gè)技巧,我是從《Is parallel programming hard?》上學(xué)到的。
聽(tīng)起來(lái)都很好?然而險(xiǎn)象環(huán)生:volatile常被誤用,很多人往往不知道或者忽略它的兩個(gè)特點(diǎn):在C/C++語(yǔ)言里,volatile不保證原子性;使用volatile不應(yīng)該對(duì)它有任何Memory Barrier的期待。
第一點(diǎn)比較好理解,對(duì)于第二點(diǎn),我們來(lái)看一個(gè)很經(jīng)典的例子:
- volatile int is_ready = 0;
- char message[123];
- void thread_A
- {
- while(is_ready == 0)
- {
- }
- //use message;
- }
- void thread_B
- {
- strcpy(message,"everything seems ok");
- is_ready = 1;
- }
線(xiàn)程B中,雖然is_ready有volatile修飾,但是這里的volatile不提供任何Memory Barrier,因此12行和13行可能被亂序執(zhí)行,is_ready = 1被執(zhí)行,而message還未被正確設(shè)置,導(dǎo)致線(xiàn)程A讀到錯(cuò)誤的值。
這意味著,在多線(xiàn)程中使用volatile需要非常謹(jǐn)慎、小心。
__thread
__thread是gcc內(nèi)置的用于多線(xiàn)程編程的基礎(chǔ)設(shè)施。用__thread修飾的變量,每個(gè)線(xiàn)程都擁有一份實(shí)體,相互獨(dú)立,互不干擾。舉個(gè)例子:
- #include
- #include
- #include
- using namespace std;
- __thread int i = 1;
- void* thread1(void* arg);
- void* thread2(void* arg);
- int main()
- {
- pthread_t pthread1;
- pthread_t pthread2;
- pthread_create(&pthread1, NULL, thread1, NULL);
- pthread_create(&pthread2, NULL, thread2, NULL);
- pthread_join(pthread1, NULL);
- pthread_join(pthread2, NULL);
- return 0;
- }
- void* thread1(void* arg)
- {
- coutiendl;//輸出 2
- return NULL;
- }
- void* thread2(void* arg)
- {
- sleep(1); //等待thread1完成更新
- coutiendl;//輸出 2,而不是3
- return NULL;
- }
需要注意的是:
1,__thread可以修飾全局變量、函數(shù)的靜態(tài)變量,但是無(wú)法修飾函數(shù)的局部變量。
2,被__thread修飾的變量只能在編譯期初始化,且只能通過(guò)常量表達(dá)式來(lái)初始化。
Memory Barrier
為了優(yōu)化,現(xiàn)代編譯器和CPU可能會(huì)亂序執(zhí)行指令。例如:
- int a = 1;
- int b = 2;
- a = b + 3;
- b = 10;
CPU亂序執(zhí)行后,第4行語(yǔ)句和第5行語(yǔ)句的執(zhí)行順序可能變?yōu)橄萣=10然后再a=b+3
有些人可能會(huì)說(shuō),那結(jié)果不就不對(duì)了嗎?b為10,a為13?可是正確結(jié)果應(yīng)該是a為5啊。
哦,這里說(shuō)的是語(yǔ)句的執(zhí)行,對(duì)應(yīng)的匯編指令不是簡(jiǎn)單的mov b,10和mov b,a+3。
生成的匯編代碼可能是:
- movl b(%rip), %eax ; 將b的值暫存入%eax
- movl $10, b(%rip) ; b = 10
- addl $3, %eax ; %eax加3
- movl %eax, a(%rip) ; 將%eax也就是b+3的值寫(xiě)入a,即 a = b + 3
這并不奇怪,為了優(yōu)化性能,有時(shí)候確實(shí)可以這么做。但是在多線(xiàn)程并行編程中,有時(shí)候亂序就會(huì)出問(wèn)題。
一個(gè)最典型的例子是用鎖保護(hù)臨界區(qū)。如果臨界區(qū)的代碼被拉到加鎖前或者釋放鎖之后執(zhí)行,那么將導(dǎo)致不明確的結(jié)果,往往讓人不開(kāi)心的結(jié)果。
還有,比如隨意將讀數(shù)據(jù)和寫(xiě)數(shù)據(jù)亂序,那么本來(lái)是先讀后寫(xiě),變成先寫(xiě)后讀就導(dǎo)致后面讀到了臟的數(shù)據(jù)。因此,Memory Barrier就是用來(lái)防止亂序執(zhí)行的。具體說(shuō)來(lái),Memory Barrier包括三種:
1,acquire barrier。acquire barrier之后的指令不能也不會(huì)被拉到該acquire barrier之前執(zhí)行。
2,release barrier。release barrier之前的指令不能也不會(huì)被拉到該release barrier之后執(zhí)行。
3,full barrier。以上兩種的合集。
所以,很容易知道,加鎖,也就是lock對(duì)應(yīng)acquire barrier;釋放鎖,也就是unlock對(duì)應(yīng)release barrier。哦,那么full barrier呢?
__sync_synchronize
__sync_synchronize就是一種full barrier。