所謂原子操作,就是"不可中斷的乙個或一系列操作" , 在確認乙個操作是原子的情況下,多執行緒環境裡面,我們可以避免僅僅為保護這個操作在外圍加上效能昂貴的鎖,甚至借助於原子操作,我們可以實現互斥鎖。
很多作業系統都為int型別提供了+-賦值的原子操作版本,比如 nt 提供了 interlockedexchange 等api, linux/unix也提供了atomic_set 等函式。
前兩天有同學問我:在x86上,g_count++ (int型別) 是否是乙個原子操作? 我的回答是"不是的, 多個cpu的機器(smp)上面這就不是原子操作"。
今天想起,在單cpu上這個是否是原子操作呢,但是這個和編譯器有關,編譯器可能有兩種編譯方式:
a. 多條指令版本 , 這就不是原子的
mov 暫存器 , g_count
add 暫存器, 1
mov g_count , 暫存器
b. 單指令版本, 這在單cpu的x86上就是原子的
inc g_count
只能寫程式驗證了, 讓5個執行緒每個對 g_count++ 一億次,假如是原子操作的話,結果應該是5億:
其實還需要對 g_count 進行volatile宣告,防止編譯器對這裡不適當的優化,為了看看編譯器對volatile的處理,我另外做了個
volatile
版本作為比較。
#include
>
#include
>
int g_count = 0;
dword winapi threadfunc( lpvoid lpparam
)#define thread_num
5void main( void )}
printf(
"press any key after all thread exit.../n"
);getchar
();printf(
"g_count %d/n"
, g_count
);if
(g_count!=thread_num*
100000000
)getchar
();//乙個隨手的程式,就不close handle了
}volatile的本意是易變的, 它限制編譯器的優化,因為cpu對暫存器處理比記憶體快很多,我想這個程式的沒有加上volatile的版本優化以後應該是這樣:
mov 暫存器, g_count
for迴圈一億次, 執行 inc 暫存器
mov
g_count, 暫存器
這樣,最後
g_count的值應該是
1億,2億,
3億,4億,
5億的整數,1億出現的可能性較高。
而加上volatile以後,或者是沒有**優化的版本,都是老老實實對記憶體加上一億次,假如不是原子操作的話,最後結果就會比五億小。
用的是vc6的cl編譯器,我預期的結果是這樣的:
++是原子操作
沒有**優化
**優化(cl -o2編譯)
沒有 volatile
g_count == 五億
g_count的值應該是
1億,2億,
3億,4億,
5億的整數
volatile
g_count == 五億
g_count == 五億
++ 不是原子操作
沒有**優化
**優化(cl -o2編譯)
沒有 volatile
g_count < 五億
g_count的值應該是
1億,2億,
3億,4億,
5億的整數,1億出現的可能性較高
volatile
同上g_count < 五億
但是最後的結果卻讓我大跌了一下眼鏡:
vc6實驗的結果
沒有**優化
**優化
沒有 volatile
g_count 一般為五億, 偶爾< 五億(疑惑中...)
都是五億(疑惑中...)
volatile
同上(疑惑中...)
g_count = < 五億(這個可以解釋)
這個結果太讓人疑惑了,沒辦法,只能看asm**了, 首先看看為什麼volatile的版本為什麼和預期不符合吧:
for
(i=0
; i <
100000000
; i++)
初始化i=0;
mov dword ptr _i$[ebp], 0
jmp short $l52751
$l52752: i++
mov ecx, dword ptr _i$[ebp]
add ecx, 1
mov dword ptr _i$[ebp], ecx
$l52751:
判斷 i <100000000
cmp dword ptr _i$[ebp], 100000000 ; 05f5e100h
jge short $l52753
g_count
++;//這裡發現編譯使用的是多個指令,也就是說
g_count
++不是原子的
mov edx, dword ptr _g_count
add edx, 1
mov dword ptr _g_count, edx
jmp short $l52752
//初始化 i = 100000000, 這個迴圈變數被直接放到了暫存器裡面
mov eax, 100000000 ; 05f5e100h
$l52793:
//g_count
++;這裡發現編譯使用的是多個指令,也就是說
g_count
++不是原子的
mov ecx, dword ptr _g_count
inc ecx
mov dword ptr _g_count, ecx
//下面又是迴圈體的asm**
dec eax // i--
jne short $l52793 // if (i>0) 則繼續迴圈
終於發現了問題所在了, 優化以後,迴圈從i++變成了i--, 就是如下的形式:
for
(i=100000000
; i >
0 ; i
--)g_count
++;因為將乙個數字和0比較和將其與其他數字比較更加有效率優勢,而且這裡i在迴圈體裡面並不使用,所以vc編譯器將其變換成上面的形式,可以大大節省迴圈執行的時鐘週期。
這樣,未優化的版本有很大的機會出現 g_count == 五億 就有了解釋,是因為:
cpu對於純粹的整數運算是很快的,一億次迴圈裡面,可能只有一兩次的執行緒上下文切換
沒有優化的版本迴圈體比++操作本身更加耗時,這樣切換操作很可能出現在 for 迴圈中, 而不是 g_count
++的三條指令之間
這裡也證明了vc6編譯器對於 ++ 的執行**是是非原子的,查了一下資料 這3條指令在pentium以後的cpu比一條inc更快
發現彙編**的迴圈體完全沒有了:
mov eax, dword ptr _g_count
push esi
add eax, 100000000 ; 05f5e100h
表示成c的**大概就是這樣: g_count+=100000000; 編譯器還是很聰明,發現這個迴圈其實使用前面的語句也可以達到目的,乾脆把迴圈拿掉了,這樣因為執行緒執行時間很短,往往乙個執行緒都執行完了其他執行緒還沒有被排程,所以結果都是5億了。
附帶以下總結:
1. 不要小看編譯器的聰明程度,上面的那些優化,我在gcc上也作了驗證,我們不要太在意i++/++i之類的優化,要相信編譯器能做好它
2. ++的操作在單cpu的x86上也不是原子性的,所以優化多執行緒效能的兄弟不要在這裡搞過火,老實用interlockedincrement吧
3. x86上,不管是否smp, 對於int(要求位址4 bytes對齊)的讀取和賦值還是原子的,不過這個就和這個試驗無關了(risc的機器就不要這樣做了,大家還是加鎖吧)
volatile的非原子性
package com.freeflying.thread.volatil volatile的非原子性 classname volatilenotatomic description author freeflying date 2018年7月14日 public class volatilenot...
volatile不能保證原子性
在討論原子性操作時,我們經常會聽到乙個說法 任意單個volatile變數的讀寫具有原子性,但是volatile 這種操作除外。所以問題就是 為什麼volatile 不是原子性的?因為它實際上是三個操作組成的乙個符合操作。首先獲取volatile變數的值 將該變數的值加1 將該volatile變數的值...
volatile不保證原子性
1.什麼是原子性?不可分割 完整性,即某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗 2.寫乙個demo來驗證volatile不保證原子性 大概率結果不是2000 因為i 不是一步操作,而不是一步操作,所以無法保證原子性 class source...