Volatile關鍵字與原子性操作

2021-10-19 07:45:45 字數 3304 閱讀 2569

原子性操作具有不可分割性。比如 i=0 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:

i++;
這個操作實際是

i = i +

1;

那麼x=y;這個操作是不是原子性操作呢?答案也是否定的,該操作可以向下細分為首先讀取y的資料,再賦值給x的過程。

注意:在32位平台下,對64位資料的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性。

volatile 關鍵字是一種型別修飾符,被它宣告的型別變數表示可以被某些編譯器未知的因素更改,比如:作業系統、硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的**就不再進行優化,從而可以提供對特殊位址的穩定訪問。宣告時語法如下:

int

volatile vint;

計算機在執行程式時,每條指令都是在cpu中執行的,而在執行指令過程中,會涉及到資料的讀取和寫入。由於程式執行過程中的臨時資料是存放在主存(物理記憶體)當中的,這時就存在乙個問題,由於cpu執行速度很快,而從記憶體讀取資料和向記憶體寫入資料的過程跟cpu執行指令的速度比起來要慢的多,因此如果任何時候對資料的操作都要通過和記憶體的互動來進行,會大大降低指令執行的速度。因此在cpu裡面就有了快取記憶體。

也就是,當程式在執行過程中,會將運算需要的資料從主存複製乙份到cpu的快取記憶體當中,那麼cpu進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。

而當要求使用 volatile 宣告的變數的值的時候,系統總是重新從它所在的記憶體讀取資料,即使它前面的指令剛剛從該處讀取過資料。而且讀取的資料立刻被儲存。

同樣針對i++;操作。當執行緒執行這個語句時,會先從主存當中讀取i的值,然後複製乙份到快取記憶體當中,然後cpu執行指令對i進行加1操作,然後將資料寫入快取記憶體,最後將快取記憶體中i最新的值重新整理到主存當中。

這個**在單執行緒中執行是沒有任何問題的,但是在多執行緒中執行就會有問題了。在多核cpu中,每條執行緒可能執行於不同的cpu中,因此每個執行緒執行時有自己的快取記憶體(對單核cpu來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。本文我們以多核cpu為例。

當同時有2個執行緒執行這段**,假如初始時i的值為0,那麼我們希望兩個執行緒執行完之後i的值變為2。在實際執行過程中可能存在下面一種情況:最初,兩個執行緒分別讀取i的值存入各自所在的cpu的快取記憶體當中,然後執行緒1進行加1操作,然後把i的最新值1寫入到記憶體。此時執行緒2的快取記憶體當中i的值還是0,進行加1操作之後,i的值為1,然後執行緒2把i的值寫入記憶體。

最終結果i的值是1,而不是2。這就導致了結果的出錯。通常稱這種被多個執行緒訪問的變數為共享變數。

也就是說,如果乙個變數在多個cpu中都存在快取(主要當多執行緒時),那麼就可能存在快取不一致的問題。

針對這類問題從硬體角度講可以通過在匯流排加lock鎖或者通過快取一致性協議的方式實現。軟體方面不同的語言存在著不同的操作。

在併發程式設計中,多程序的乙個共享變數被volatile修飾之後,那麼就具備了兩層語義:

1)保證了不同執行緒對這個變數進行操作時的可見性,即乙個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。

2)禁止進行指令重排序。(保證指令的有序性)

通過例子來說明第一層語義,假設執行緒1先執行,執行緒2後執行:

//執行緒1

boolean stop = false;

while

(!stop)

//執行緒2

stop = true;

針對這個程式存在以下的問題:每個執行緒在執行過程中都有自己的工作記憶體,那麼執行緒1在執行的時候,會將stop變數的值拷貝乙份放在自己的工作記憶體當中。那麼當執行緒2更改了stop變數的值之後,但是還沒來得及寫入主存當中,執行緒2轉去做其他事情了,那麼執行緒1由於不知道執行緒2對stop變數的更改,因此還會一直迴圈下去。

但是用volatile修飾之後就變得不一樣了:

首先使用volatile關鍵字會強制將修改的值立即寫入主存;

之後使用volatile關鍵字的話,當執行緒2進行修改時,會導致執行緒1的工作記憶體中快取變數stop的快取行無效(反映到硬體層的話,就是cpu的l1或者l2快取中對應的快取行無效);

最後由於執行緒1的工作記憶體中快取變數stop的快取行無效,所以執行緒1再次讀取變數stop的值時會去主存讀取。

那麼執行緒1讀取到的就是最新的正確的值。

但是volatile關鍵字仍然不能保證原子性。

問題在於volatile關鍵字能保證可見性沒有錯,但是上面的程式錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變數的操作的原子性。

在前面已經提到過,自增操作是不具備原子性的,它包括讀取變數的原始值、進行加1操作、寫入工作記憶體。那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導致快取不同步的情況出現。

通常volatile關鍵字被用於以下的幾個地方:

1、中斷服務程式中修改的供其它部分程式檢測的共享變數。

2、多工環境下各個任務的共享標誌應該加volatile。

3、儲存器的硬體暫存器需要加volaile說明,因為對它的每次讀寫都有不同的含義。

在這裡簡單的提供乙個使用倒原子性操作的例程,並記錄一些注意事項。

統計計算一組資料並作出直方圖:

cpu版本:

#define size (100*1024*1024)

intmain

(void

)}

這個方法比 cpu 還要慢,因為幾千個執行緒訪問少量的記憶體,將發生大量的競爭,為了確保遞增操作的原子性,對相同記憶體位置的操作都將被硬體序列化。

使用共享記憶體原子操作和全域性記憶體原子操作的直方圖kernel函式

__global__ void

histo_kernel

(char

*buffer,

long size,unsinged int

*histo)

__syncthreads()

;//最後將所用共享記憶體上的直方圖相加到乙個直方圖上

atomicadd(&

(histo[threadidx.x]

),temp[threadidx.x]);

}

雖然最初將直方圖程式修改為gpu版本使效能下降,但同時使用共享記憶體與原子性操作仍然能使程式加速。實測程式速度提公升了大概7倍左右。

關鍵字 volatile關鍵字的作用

1.volatile關鍵字是防止在共享的空間發生讀取的錯誤。只保證其可見性,不保證原子性 使用volatile指每次從記憶體中讀取資料,而不是從編譯器優化後的快取中讀取資料,簡單來講就是防止編譯器優化。2.在單任務環境中,如果在兩次讀取變數之間不改變變數的值,編譯器就會發生優化,會將ram中的值賦值...

關鍵字volatile詳解

與關鍵字const一樣,關鍵字volatile也是乙個型別修飾符 type specifier 關於volatile的作用 例如程式清單 volatile int m 10 int k,n m k i volatile將告訴編譯器,整形變數m是隨時發生變化的,每次使用m的時候,都要求從m的位址中找出...

volatile關鍵字 詳解

volatile 關鍵字 volatile關鍵字是一種型別修飾符,用它宣告的型別變數表示可以被某些編譯器未知的因素更改,比如 作業系統 硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的 就不再進行優化,從而可以提供對特殊位址的穩定訪問。使用該關鍵字的例子如下 int volati...