淺談原子操做、volatile、CPU執行順序

淺談原子操做、volatile、CPU執行順序

在計算機發展的鴻蒙年代,程序都是順序執行,編譯器也只是簡單地翻譯指令,隨着硬件和軟件的飛速增加,原來的工具和硬件漸漸地力不從心,也逐漸涌現出各路大神在原來的基礎上進行優化,有些優化是徹底地升級,而有些優化則是創建在犧牲其餘性能之上,固然這種優化在大多數狀況下是正向的,只是在某些時候會體現出負面的效果,今天咱們就來談談那些因爲軟硬件的優化產生的問題。html

原子操做和鎖機制

學過C語言的咱們都知道一個概念:程序是順序執行的。可是因爲操做系統的存在,這個概念變成了局部適用,由於操做系統的工做就是讓多任務併發運行(單CPU下,雖然底層仍然是順序執行,至少從用戶角度來講,任務是併發運行的)。linux

操做系統的發明簡直就是一次革命,尤爲是桌面操做系統的盛行,對人類設備的發展起到了很是大的做用。程序員

對於單核CPU而言,操做系統實現多任務的方式就是經過中斷將時間不斷地分片,經過某種調度手法,讓多個任務循環地佔用CPU的執行時間,形成多任務在同時運行的假象,固然,前提是CPU的運行頻率足夠快,用戶感受不到任務之間的切換。算法

可是這就帶來一個問題,若是某個任務須要作一個不能被打斷的任務,反而沒那麼容易。緩存

既然提出問題,固然就有解決方案,就是原子操做。多線程

原子操做

咱們在化學課上學到,在目前的知識體系下,原子是不可分割的,原子操做所以而得名。併發

它代表:操做要麼不進行,要麼就直到執行完,不會被其餘線程打斷。在linux下,定義了兩種原子操做,分別是int型變量操做和位操做方式。工具

它的定義是這樣的:性能

typedef struct {
    int counter;
} atomic_t;

能夠看到,atomic_t類型的原子操做就是針對int型變量進行操做。優化

原子操做的API:

atomic_read(atomic_t *v)             //讀原子變量
atomic_set(atomic_t *v,int i)       //設置原子變量值
atomic_add(int i, atomic_t *v)       //原子變量加i
atomic_sub(int i, atomic_t *v)       //原子變量減i
....                                 //

看到這裏有些朋友就有疑問了,int變量的操做難道不就是原子操做嗎?就像:

int i = 0 
i++

這應該是原子操做吧。

可是事實並不是如此,int i = 0,這條賦值語句確實是賦值語句,可是i++並不是是原子操做,它包含如下的操做:

一、從內存中取出i
二、i加1
三、將i寫回內存

那麼,爲何須要原子操做?咱們能夠參考上面的三個步驟,若是在執行完第一步時,另外一個線程被調度執行,恰好也操做到i(i爲全局變量),操做完以後再回到當前線程,此時i的值仍是在原來的值上加1,這就違背了程序的意圖:咱們能夠看下面的過程,設i爲全局變量,初始值爲5.

能夠看到,若是不進行原子操做,i經歷了兩次++,值僅僅是增長了1,顯然有問題,因此須要用到原子操做。

除了int型原子操做,linux下還支持設置位的原子操做,本文並不去細究原子操做細節,就再也不贅述。

鎖機制

上述原子操做的效果能夠當作:要操做的數據在操做開始直到操做結束,不會被其餘任務所影響,從數據的角度而不是操做的角度出發,還有一種機制能夠實現防止數據被其它任務破壞,就是鎖機制,通常是使用互斥鎖。

可是須要注意的是,鎖機制並非保證原子操做,它只是防止其餘任務操做不想被操做的數據,並且咱們須要知道的是,計算機的運行就是對數據的處理,鎖住的對象應該是數據而非指令,不一樣於原子操做的是,在操做加鎖數據時,可能出現任務調度而執行其餘程序,可是它能夠保護數據不被其餘程序操做,在執行效果上與原子操做實際上是大同小異的,即一個任務要處理部分數據,從操做開始到操做完成,這部分數據都只會被當前任務所影響。

同時,原子操做目前在各大平臺上通常都只支持int型和bit操做,對於複雜的,大塊的數據,原子操做顯然力不從心。

volatile

在數據操做的多線程同步上是否是使用了原子操做就萬事大吉了呢?非也非也!!

gcc編譯器爲了提升執行效率,和硬件相配合作了一件事,就是採用緩存機制,緩存數據的好處就是提升效率,具體的操做就是對代碼進行優化。

當程序在運行時,若是每次讀寫數據都直接從內存讀寫,效率很快就達到瓶頸,編譯器在這裏作的優化就是若是一個數據會被頻繁使用,就會被緩存在寄存器或者高速緩存中,下一次再使用的時候就不須要從新從內存中讀取,直接從寄存器或者高速緩衝中讀取便可,這樣就大幅減小了數據訪問時間,達到提升程序運行效率的效果。

可是,在多線程和中斷程序的的環境下,這種編譯器執行的優化將會帶來同步問題。

要理解這種同步問題,首先咱們須要創建一個概念:多線程共享數據空間,可是擁有各自的棧數據和寄存器數據備份,當輪到本線程運行時,系統將上一線程的寄存器數據,堆棧數據保存起來,而後將當前線程的運行數據恢復到寄存器中,設置PC指針跳轉執行當前線程。

咱們看下面的例子:

從上面的例子能夠看到,儘管線程中使用鎖機制來保障數據在操做時不被其餘任務所幹擾,可是因爲數據被緩存而致使線程之間的數據出現同步問題。

話說回來,編譯器也不至於那麼蠢,對於任何指令都無腦進行緩存的優化,它會在緩存數據的同時會分析數據之間的依賴關係,可是,在多線程或者中斷程序中,因爲程序的執行是一種非預約義行爲,編譯器的優化可能並不能考慮到這一點,因此,在編寫多線程程序時,對於跨線程的全局變量,或者在中斷中的全局變量,必定要用volatile進行修飾,否則將發生很是難以調試的bug。

至此,編譯器開發人員漸漸意識到這種優化帶來的問題,便增長了一個關鍵字:volatile,volatile關鍵字聲明的數據,即告訴編譯器,全部由volatile修飾的數據都不要進行優化,每次的讀寫都老老實實地從內存中讀出而後寫回。

至於使用方法,和static const同樣:

volatile int x;

volatile修飾和原子操做

咋一看,這兩個東西好像是同樣的,可是仔細一瞧,咱們仍是能夠看出他們之間的區別:

  • 原子操做和鎖強調的是數據在任務執行過程當中不會被其餘任務操做到而產生衝突。

  • volatile關鍵詞的修飾則強調數據在存取時直接操做內存,而不要緩存機制對其進行緩存。

一個解決的是同時操做帶來的問題,一個解決的是操做完以後是否緩存(編譯器優化)帶來的問題。

CPU的亂序執行

在計算機工程領域,亂序執行(錯序執行,英語:out-of-order execution,簡稱OoOE或OOE)是一種應用在高性能微處理器中來利用指令週期以免特定類型的延遲消耗的範式。

在這種範式中,處理器在一個由輸入數據可用性所決定的順序中執行指令,而不是由程序的原始數據所決定。在這種方式下,能夠避免由於獲取下一條程序指令所引發的處理器等待,取而代之的處理下一條能夠當即執行的指令。 --維基百科

通俗地說,CPU內有多個計算單元,亂序執行的目的就是儘量多的地同時調動CPU內運算單元來提升運算效率(基於某種成熟算法),可是,編譯器編譯出來的代碼並不是能達到這個效果,因此就須要CPU本身來調整代碼執行順序,舉個例子,若是咱們要打開筆記本開始工做,流程大概是這樣的:

拿出筆記本並按下開機鍵 -> 等待開機完成 -> 插上鼠標鍵盤 -> 打開軟件開始修改bug

可是,若是遵守這個流程,在等待開機完成那段時間就是明顯的浪費,因此,聰明的程序員通常會這樣:

拿出筆記本並按下開機鍵 -> 等待開機完成 
                        插上鼠標鍵盤
                        回想昨天的bug,理清思路  -> 打開軟件開始修改bug

這樣,就明顯節省了整個流程的時間。

CPU也能夠是這樣,儘管你的指令告訴它應該一步一步來,可是它以爲有更高效的作法,並且還不用考慮人(機)道主義,若是能榨乾它的最後一點價值,請儘管去作。

到這裏咱們大概已經理解了CPU亂序執行的緣由,可是遺憾的是,對於CPU的亂序執行優化並不是有一個統一標準,由於更多地涉及到硬件,因此每每各廠商之間都有不一樣的優化策略,優化效果也是不盡相同。

咱們看下面的例子:

a = 1
b = 1

上面簡單的兩條語句,在這樣一種狀況下會致使第二條比一條先執行:

操做a的時間須要等待的時間較長,而b不須要。比較一般的狀況就是a在內存中,而b被緩存在寄存器或者高速緩存中且可用,對a發出讀寫指令後,須要等待。  

CPU檢測這兩條語句之間並無依賴性,能夠調整執行順序。  

先執行b = 1,再執行a = 1,以提升效率。

在單核的狀況下這是沒有問題的,主要是由於CPU能判斷兩條語句之間是否存在依賴關係,可是若是放在多核系統上就會出現麻煩,咱們來看看下面的例子:


在上述提到的狀況中,CPU0亂序執行,先執行了b = 1,此時a還不等於1,因此在CPU1在檢測到b!=1時,立馬執行assert(a == 1),致使assert報錯。

內存屏障

爲了解決CPU亂序執行而帶來的問題,內存屏障應運而生,許多CPU都提供內存屏障指令,內存屏障指令也是平臺各異的,經典的X86下有ifence、mfence、sfence指令。

在Visual C++2005標準中,保證volatile提供一種內存屏障,組織編譯器和CPU從新安排讀入和寫出。

PowerPC上則是lwsync。咱們把內存屏障指令插入不想被優化的指令以後便可達到相應的目的。

小結

在多線程中,爲了解決多個線程同時操做同一份數據而帶來同步問題,產生了原子操做,鎖機制。

在緩存機制中,爲了解決程序對數據的緩存而致使數據同步問題,增長了volatile關鍵詞修飾。

在多核系統中,因爲CPU亂序執行可能帶來的問題,產生了內存屏障機制,以防止內存優化帶來的問題。

參考:http://www.voidcn.com/article/p-fiewgxpd-bbh.html

《程序員的自我修養--連接、裝載與庫》

好了,關於原子操做、volatile、CPU執行順序的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索