C# Volatile的具體使用
1.Overview
經常研究.NET源碼庫的小夥伴會經常看到一個關鍵字volatile,那它在開發當中的作用是什麼呢?
我們一起來看看官方文檔裡是怎麼描述的,如下:
“volatile
關鍵字指示一個字段可以由多個同時執行的線程修改。出於性能原因,編譯器,運行時系統甚至硬件都可能重新排列對存儲器位置的讀取和寫入。聲明為 volatile
的字段將從某些類型的優化中排除。不確保從所有執行線程整體來看時所有易失性寫入操作均按執行順序排序。”
本文將圍繞這部分進行解讀。
聲明語法如下:
class VolatileTest { public volatile int sharedStorage; public void Test(int i) { sharedStorage = i; } }
2.Detail
我們先瞭解一下前置知識點。
(1)在CLR中將對sbyte
、byte
、short
、ushort
、int
、uint
、char
、float
和 bool
。以及引用類型保證讀寫時原子性的(long、double不是原子性讀寫)變量中的所有字節都是一次性寫入或讀取的。
(2)Framework Class Library(FCL) 保證所有靜態方法都是線程安全的。這意味著假如兩個線程同時調用一個靜態方法,不會有數據被損壞。為什麼?
public static string Print(String str) { string val = ""; val += str; return val; }
因為靜態方法內聲明的變量,每個線程調用時都會新創建一份,而不會共用一個存儲單元。比如這裡的val每個線程都會創建自己的一份,因此不會有線程安全問題。註意:靜態變量,由於是在類加載時占用一個存儲區每個線程都是共用這個存儲區的,所以如果在靜態方法裡使用瞭靜態變量;這就會有線程安全問題。
(3)內存、CPU緩存(註:下列為簡述內容,實際上不僅如此)
CPU緩存,CPU集成的緩存。
內存,內存條硬件提供的存儲空間。
我們繼續回到主要內容上,用下面的若幹代碼示例來表達volatile的作用。
public class Program { public static int bookNum = 0; public static void Main(string[] args) { Console.WriteLine("juster書的數量:" + bookNum); Thread juster = new Thread(() => { Console.WriteLine("juster沒帶書,等待傢長送書到學校..."); while (bookNum == 0) {} Console.WriteLine("juster拿到書,開始上課聽講。"); }); juster.Name = nameof(juster); juster.Start(); Thread parent = new Thread(() => { Console.WriteLine("parent在屋裡找書中..."); Thread.Sleep(2000); Console.WriteLine("parent找到瞭書之後,送往學校..."); SendBook(); }); parent.Name = nameof(parent); parent.Start(); } public static void SendBook() { bookNum = 1; } }
代碼執行輸出如下:
這時候詭異的來瞭,按照正常的代碼執行邏輯不難看出當parent線程執行Sendbook()的時候juster應該就能拿到書上課瞭。但是這裡juster卻一直沒有拿到是為什麼呢?
心細的小夥伴應該觀察到瞭這裡的運行模式是Release,眾所周知Release是.Net的發佈版本執行效率會比Debug版本要高。
為什麼Release版本效率高呢?怎麼得來的?下面這段代碼來解釋:
上面這張反編譯的圖不難看出,10*10-100這段代碼直接編譯成0瞭。這種現象是因為Release編譯的時候編譯器會對代碼進行‘優化’。這段是最直觀能看到的‘優化’效果,其實C#編譯器將你的代碼轉換成中間語言(IL)。然後,JIT將IL轉換成本機CPU指令。此外,C#編譯器、JIT編譯器,甚至CPU本身都可能優化你的代碼。
但是實際上在上述代碼中count的值始終為0;所以循環永遠不會執行,沒有必要編譯循環內的代碼在編譯後會被‘優化’。說瞭這麼多,隻是為瞭給大夥證明Release編譯這一層會存在‘優化’;接下來繼續回到volatile上。
說到這裡,如何解決各種‘優化’帶來的問題呢?這時候隻需要在booknum前面加上volatile關鍵字修飾即可。
public class Program { public static volatile int bookNum = 0; public static void Main(string[] args) { Console.WriteLine("juster書的數量:" + bookNum); Thread juster = new Thread(() => { Console.WriteLine("juster沒帶書,等待傢長送書到學校..."); while (bookNum == 0) { } Console.WriteLine("juster拿到書,開始上課聽講。"); }); juster.Name = nameof(juster); juster.Start(); Thread parent = new Thread(() => { Console.WriteLine("parent在屋裡找書中..."); Thread.Sleep(2000); Console.WriteLine("parent找到瞭書之後,送往學校..."); SendBook(); }); parent.Name = nameof(parent); parent.Start(); } public static void SendBook() { bookNum = 1; } }
在被各種優化之後,booknum因為是值類型在每個線程訪問時會發生復制且又是在靜態方法中被修改。所以每個線程都會復制booknum的值到當前線程上下文中緩存起來。這樣就導致瞭parent線程修改瞭booknum的值juster線程看不到的情況。這個時候就需要用volatile關鍵字告訴編譯器不需要這樣的優化,表示用volatile定義的變量會被改變,每次都必須從內存中讀取,而不能把他放在CPU cache或寄存器中重復使用。最後booknum會在運行的過程中修改值且其他線程能‘共享訪問’達到最終的效果。
3.Conclusion
Part1
volatile
關鍵字可應用於以下類型的字段:
- 引用類型。
- 指針類型(在不安全的上下文中)。請註意,雖然指針本身可以是可變的,但是它指向的對象不能是可變的。換句話說,不能聲明“指向可變對象的指針”。
- 簡單類型,如
sbyte
、byte
、short
、ushort
、int
、uint
、char
、float
和bool
。 - 具有以下基本類型之一的
enum
類型:byte
、sbyte
、short
、ushort
、int
或uint
。 - 已知為引用類型的泛型類型參數。
- IntPtr 和 UIntPtr。
其他類型(包括 double
和 long
)無法標記為 volatile
,因為對這些類型的字段的讀取和寫入不能保證是原子的。若要保護對這些類型字段的多線程訪問,請使用 Interlocked 類成員或使用 lock
語句保護訪問權限。
volatile
關鍵字隻能應用於 class
或 struct
的字段。不能將局部變量聲明為 volatile
。
Part2
volatile並不能用來做線程同步,它的主要作用時為瞭讓多個線程之間能看到被修改過後最新的值。
Part3
C#不支持以傳遞引用的方式將volatile字段傳給方法。
int.TryParse("123", out x);
Part4
除瞭禁止編譯優化,還有同步到內存中因為CPU每個核心都有自己Cache所以需要同步到內存中方便其他核心使用。
Part5
看完本文也能解開小白時期的疑惑,為什麼我寫代碼編譯成release版本之後就不能運行報錯的奇特現象瞭。
Part6
volatile
牽扯到的相關知識點和原理遠遠不止這些。
4.Reference
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile?WT.mc_id=WDIT-MVP-5004326
到此這篇關於C# Volatile的具體使用的文章就介紹到這瞭,更多相關C# Volatile內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!