前言
假如你寫過多執行緒系統一定會看過volatile
,但你對他的了解有多少?
MSDN對於volatile關鍵字解釋如下.
volatile 關鍵字指出某個欄位可能是由同時執行的多個執行緒所修改。 編譯器、執行階段系統,甚至硬體都有可能基於效能因素,而重新排列對記憶體位置的讀取和寫入。 宣告為 volatile 的欄位不受限於這些最佳化考量。 加入 volatile 修飾詞可確保所有的執行緒都會依執行寫入的順序,觀察任何其他執行緒所執行的暫時性寫入。
MSDN上寫一堆文謅謅的解釋,如果沒有相對應OS或底層概念會很難理解上面敘述
volatile 三大特性
這裡我先總結volatile
三大特性
volatile
修飾的變數具有可見性volatile
避免指令優化重排volatile
不保證Atomic
本文會針對這三大特性來一一解釋
注意在執行本文程式碼時要把Build Mode改成Release.
volatile令變數具有可見性 & volatile避免指令優化重排
下面有段程式碼.
有一個member
物件初始數值Balance=100
,建立一個Thread裡面會把member
物件的餘額成0
在Main Thread中while (member.balance > 0)
有一段程式會等待member.balance=0
跳出迴圈.
預期在程式最後印出執行結束!
但如果您使用Release Mode來跑會發現
最後一行執行結束!
不會如預期印出來..
1 | class Program |
解釋volatile可見性 & 指令優化重排
每個Thread對於變數操作,會先把Memory記憶體中的變數Copy一份到記憶體中並執行操作,操作完畢再重新寫入Memory中.
概念大致如下圖
所以這就會導致一個問題,假如ThreadA對於變數做異動,但ThreadB不會被通知
一般來說Release Mode,會把程式語言優化(包含一些CPU指令),所以在multiple-Thread 系統中可能就會遇到一些不預期問題(如此範例)
所以依照上面特性我來總結解釋一下
- 一開始Main Thread跟sub Thread在取得Balance都是100
- sub Thread更新餘額成為0,但Main Thread沒有更新(還是100)
- Main Thread進入無限迴圈導致出不來.
使用 volatile 解決問題
解決上面問題我們只需要在balance
上加一個volatile
就好!
1 | public class Member |
執行結果如上,能看到程式可以正常結束了
還記得我一開始的總結前兩條嗎
volatile
修飾的變數具有可見性volatile
避免指令優化重排
使用volatile
會告訴編譯器別嘗試優化,使用此變數程式碼,無論是讀取還是寫入,都在主記憶體操作。
所以當呼叫UpdateBalance
(balance = 0;
) 方法時此異動在Main Thread就會看到Memory balance = 0
所以就跳出迴圈.
volatile 不保證Atomic
雖然volatile讓變數具有可見性,但不保證Atomic(原子性),這是甚麼意思?
我一樣用下圖來解釋來解釋.
在每個Thread要異動變數都會將數值Copy進Thread中進行修改在異動,就算目前有可見性,但我們不能保證修改指令具有原子性
所以可能造成兩個Thread剛好對於一個數值或物件異動造成Data Racing.
如果要解決此問題可以參閱 高併發系統系列-使用lock & Interlocked CAS(compare and swap)
下面的範例來演示我說的問題
1 | class Program |
我這個例子使用10個Task來模擬高併發動作,對於同一個數值做新增餘額10000次
理論上我們預期Balance要是10w,但每次執行的結果都不是10w且數值都不一樣,這個問題在正式環境很嚴重.
我們可以使用lock來避免同一時間會有多個Thread對於同一個物件修改
1 | public class NoAtomicMember{ |
修改後結果如下
區域變數或參考型別使用volatile
如果是區域變數或參考型別volatile
關鍵字就無法使用,這時候我們可以使用下面兩個method來替代使用.
Thread.VolatileRead
Thread.VolatileWrite
下面是VolatileRead
,VolatileWrite
原始碼
能發現在裡面都有呼叫MemoryBarrier
方法.
MemoryBarrier
保證我們程式可見性,概念跟volatile一樣清除cache直接讀取主要Memory資料.
1 | public static Object VolatileRead(ref Object address) |
MSDN Thread.MemoryBarrier其中有段說明
同步處理記憶體存取,如下所示:執行目前執行緒的處理器無法以下列方式重新排列指示
IL中使用volatile差異
我們知道C#是透過CLR來運行IL中繼語言,我們可以透過ILSpy
來查看上面使用volatile和沒使用volatile差異
沒使用volatile
Main function:
Member Class:
使用volatile
Main function:
Member Class:
上面IL能發現有使用volatile差異在一個指令OpCodes.Volatile
指定目前在評估Heap頂端的位址可能是volatile,並且無法快取讀取該位置的結果,或者無法隱藏存放該位置的多個存放區。
上面是MSDN解釋 主要是說使用volatile在Thread中就無法cache該變數資料每次讀取都必須回Memory中讀.
小結
在多執行緒系統中我建議常異動的變數要使用volatile
來保證每個Thread讀,寫資料是正確
發現在我們常用的Console類別,運用許多volatile
來達到Thread互相資料可見性.
但也要注意volatile
不保證Atomic,所以如果有Atomic需求記得要使用CAS或Lock來處理.
另外volatile
也不是萬靈丹,既然可以提高可見性想必對於系統會有多一些負擔,所以還是要看情況來使用.
__此文作者__:Daniel Shih(石頭)
__此文地址__: https://isdaniel.github.io/volatile-introduce/
__版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!