原子性和volatile

2021-07-16 18:08:23 字數 4144 閱讀 4858

所謂原子操作,就是"不可中斷的乙個或一系列操作" , 在確認乙個操作是原子的情況下,多執行緒環境裡面,我們可以避免僅僅為保護這個操作在外圍加上效能昂貴的鎖,甚至借助於原子操作,我們可以實現互斥鎖。

很多作業系統都為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...