說到原子,類似於以下的代碼可能人人都可以看出貓膩。 我想大多數人都知道其結果未必會得到1000000000。 測試一下吧。 可是真的知道貓膩了嗎?如果我編譯的時候優化一下呢? 運行速度一下子變的飛快,而且似乎都得到了10億。 這裡,mythread里cnt自加5億次被優化成了 cnt += 5000 ...
版權申明:本文為博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址 http://www.cnblogs.com/Colin-Cai/p/7668982.html 作者:窗戶 QQ:6679072 E-mail:[email protected]
說到原子,類似於以下的代碼可能人人都可以看出貓膩。
/* http://www.cnblogs.com/Colin-Cai */ #include <stdio.h> #include <pthread.h> int cnt = 0; void* mythread(void* arg) { int i; for(i=0;i<500000000;i++) cnt++; return NULL; } int main() { pthread_t id, id2; pthread_create(&id, NULL, mythread, NULL); pthread_create(&id2, NULL, mythread, NULL); pthread_join(id, NULL); pthread_join(id2, NULL); printf("cnt = %d\n", cnt); return 0; }
我想大多數人都知道其結果未必會得到1000000000。
測試一下吧。
linux-p94b:/tmp/testhere # gcc test1.c -lpthread linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 958925625 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000
可是真的知道貓膩了嗎?如果我編譯的時候優化一下呢?
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000
運行速度一下子變的飛快,而且似乎都得到了10億。
這裡,mythread里cnt自加5億次被優化成了 cnt += 500000000
那麼當然快啊,可是似乎這與我們當初想測試原子有那麼一些差異,一樣的代碼,不一樣的編譯,卻帶來了不同的結果。
其實原因在於,我們這裡代碼寫的不好,才沒有表達好我們當初的意思,我們是希望cnt真的自加5億次。那麼怎麼辦呢?其實很好辦,在cnt的定義前面加個volatile,那麼這裡對於cnt的自加則不會優化。很多時候,為什麼我們優化前和優化後的結果不一樣,常常是因為寫代碼的人不明白程式的優化規則。在上個公司的時候,我很想臨走的時候再給大家做一個培訓,說說C語言的優化,同時說說我們平時寫的無意依賴於編譯的所謂垃圾代碼,但是直到離開,我還是沒有做此培訓。
我們加了volatile試一下,
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done cnt = 635981117 cnt = 675792826 cnt = 522700646 cnt = 593410055 cnt = 544306380 cnt = 630888304 cnt = 580539893 cnt = 629360072 cnt = 555570127
我們在cnt定義前加個volatile,效果果然就更明顯了,因為真的是自加5億次,導致問題的機會變多了。那麼之前沒加volatile並優化編譯,會不會也有不得到10億的可能呢?
我們首先要明白的是,這裡的cnt++不是原子操作,中間有隨時調度的可能。
5億次太多,我們就拿只自加1次為例即可說明,兩個線程都只自加1次,本來期待結果為2.
cnt++在一般的處理器中至少有三條指令,我們用偽彙編來寫。
cnt -> reg //把cnt從記憶體載入到寄存器reg
reg+1 -> reg //寄存器reg自加1
reg -> cnt //把reg的內容寫入記憶體
那麼,
(線程1)cnt -> reg
(線程1)reg+1 -> reg
(線程1)reg -> cnt
(線程2)cnt -> reg
(線程2)reg+1 -> reg
(線程2)reg -> cnt
理想中,我們認為處理器的執行是以上這樣,結果cnt里的值是2。
但假設過程中發生了調度,指令執行的順序並非像以上這樣,假如變成了以下這樣
(線程1)cnt -> reg
(線程1)reg+1 -> reg
(線程2)cnt -> reg
(線程2)reg+1 -> reg
(線程2)reg -> cnt
(線程1)reg -> cnt
我們再來算算,
cnt = 0, reg任意
(線程1)cnt -> reg
cnt = 0, reg = 0
(線程1)reg+1 -> reg
cnt = 0, reg = 1
此處調度,reg = 1會被保存,併在重新調度回來之後有效,而cnt不會管
調度之後
cnt = 0, reg任意
(線程2)cnt -> reg
cnt = 0, reg = 0
(線程2)reg+1 -> reg
cnt = 0, reg = 1
(線程2)reg -> cnt
cnt = 1, reg = 1
此處又發生調度,reg會恢復之前保存的1,而cnt不會有任何變化
所以在執行下一條指令前,
cnt = 1, reg = 1
(線程1)reg -> cnt
cnt = 1, reg = 1
我們可以看到,結果成了1,而不是2,這就是非原子操作導致的結果,其實之前優化成cnt += 500000000本身也依然有此問題,只是難以觀察的到。
雖然x++不是原子,但是我們可以使用鎖的方式,來人為的製造“原子”,比如這裡用互斥。
#include <stdio.h> #include <pthread.h> volatile int cnt = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* mythread(void* arg) { int i; for(i=0;i<500000000;i++) { pthread_mutex_lock(&mutex); cnt++; pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t id, id2; pthread_create(&id, NULL, mythread, NULL); pthread_create(&id2, NULL, mythread, NULL); pthread_join(id, NULL); pthread_join(id2, NULL); printf("cnt = %d\n", cnt); return 0; }
測試一下
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000 cnt = 1000000000