C# Volatile的具體使用
1.Overview
經(jīng)常研究.NET源碼庫(kù)的小伙伴會(huì)經(jīng)??吹揭粋€(gè)關(guān)鍵字volatile,那它在開(kāi)發(fā)當(dāng)中的作用是什么呢?
我們一起來(lái)看看官方文檔里是怎么描述的,如下:
“volatile 關(guān)鍵字指示一個(gè)字段可以由多個(gè)同時(shí)執(zhí)行的線(xiàn)程修改。出于性能原因,編譯器,運(yùn)行時(shí)系統(tǒng)甚至硬件都可能重新排列對(duì)存儲(chǔ)器位置的讀取和寫(xiě)入。聲明為 volatile 的字段將從某些類(lèi)型的優(yōu)化中排除。不確保從所有執(zhí)行線(xiàn)程整體來(lái)看時(shí)所有易失性寫(xiě)入操作均按執(zhí)行順序排序?!?/p>
本文將圍繞這部分進(jìn)行解讀。
聲明語(yǔ)法如下:
class VolatileTest
{
public volatile int sharedStorage;
public void Test(int i)
{
sharedStorage = i;
}
}
2.Detail
我們先了解一下前置知識(shí)點(diǎn)。
(1)在CLR中將對(duì)sbyte、byte、short、ushort、int、uint、char、float 和 bool。以及引用類(lèi)型保證讀寫(xiě)時(shí)原子性的(long、double不是原子性讀寫(xiě))變量中的所有字節(jié)都是一次性寫(xiě)入或讀取的。
(2)Framework Class Library(FCL) 保證所有靜態(tài)方法都是線(xiàn)程安全的。這意味著假如兩個(gè)線(xiàn)程同時(shí)調(diào)用一個(gè)靜態(tài)方法,不會(huì)有數(shù)據(jù)被損壞。為什么?
public static string Print(String str)
{
string val = "";
val += str;
return val;
}
因?yàn)殪o態(tài)方法內(nèi)聲明的變量,每個(gè)線(xiàn)程調(diào)用時(shí)都會(huì)新創(chuàng)建一份,而不會(huì)共用一個(gè)存儲(chǔ)單元。比如這里的val每個(gè)線(xiàn)程都會(huì)創(chuàng)建自己的一份,因此不會(huì)有線(xiàn)程安全問(wèn)題。注意:靜態(tài)變量,由于是在類(lèi)加載時(shí)占用一個(gè)存儲(chǔ)區(qū)每個(gè)線(xiàn)程都是共用這個(gè)存儲(chǔ)區(qū)的,所以如果在靜態(tài)方法里使用了靜態(tài)變量;這就會(huì)有線(xiàn)程安全問(wèn)題。
(3)內(nèi)存、CPU緩存(注:下列為簡(jiǎn)述內(nèi)容,實(shí)際上不僅如此)
CPU緩存,CPU集成的緩存。
內(nèi)存,內(nèi)存條硬件提供的存儲(chǔ)空間。
我們繼續(xù)回到主要內(nèi)容上,用下面的若干代碼示例來(lái)表達(dá)volatile的作用。
public class Program
{
public static int bookNum = 0;
public static void Main(string[] args)
{
Console.WriteLine("juster書(shū)的數(shù)量:" + bookNum);
Thread juster = new Thread(() =>
{
Console.WriteLine("juster沒(méi)帶書(shū),等待家長(zhǎng)送書(shū)到學(xué)校...");
while (bookNum == 0) {}
Console.WriteLine("juster拿到書(shū),開(kāi)始上課聽(tīng)講。");
});
juster.Name = nameof(juster);
juster.Start();
Thread parent = new Thread(() =>
{
Console.WriteLine("parent在屋里找書(shū)中...");
Thread.Sleep(2000);
Console.WriteLine("parent找到了書(shū)之后,送往學(xué)校...");
SendBook();
});
parent.Name = nameof(parent);
parent.Start();
}
public static void SendBook()
{
bookNum = 1;
}
}
代碼執(zhí)行輸出如下:

這時(shí)候詭異的來(lái)了,按照正常的代碼執(zhí)行邏輯不難看出當(dāng)parent線(xiàn)程執(zhí)行Sendbook()的時(shí)候juster應(yīng)該就能拿到書(shū)上課了。但是這里juster卻一直沒(méi)有拿到是為什么呢?
心細(xì)的小伙伴應(yīng)該觀(guān)察到了這里的運(yùn)行模式是Release,眾所周知Release是.Net的發(fā)布版本執(zhí)行效率會(huì)比Debug版本要高。
為什么Release版本效率高呢?怎么得來(lái)的?下面這段代碼來(lái)解釋?zhuān)?/p>

上面這張反編譯的圖不難看出,10*10-100這段代碼直接編譯成0了。這種現(xiàn)象是因?yàn)镽elease編譯的時(shí)候編譯器會(huì)對(duì)代碼進(jìn)行‘優(yōu)化'。這段是最直觀(guān)能看到的‘優(yōu)化'效果,其實(shí)C#編譯器將你的代碼轉(zhuǎn)換成中間語(yǔ)言(IL)。然后,JIT將IL轉(zhuǎn)換成本機(jī)CPU指令。此外,C#編譯器、JIT編譯器,甚至CPU本身都可能優(yōu)化你的代碼。
但是實(shí)際上在上述代碼中count的值始終為0;所以循環(huán)永遠(yuǎn)不會(huì)執(zhí)行,沒(méi)有必要編譯循環(huán)內(nèi)的代碼在編譯后會(huì)被‘優(yōu)化'。說(shuō)了這么多,只是為了給大伙證明Release編譯這一層會(huì)存在‘優(yōu)化';接下來(lái)繼續(xù)回到volatile上。
說(shuō)到這里,如何解決各種‘優(yōu)化'帶來(lái)的問(wèn)題呢?這時(shí)候只需要在booknum前面加上volatile關(guān)鍵字修飾即可。
public class Program
{
public static volatile int bookNum = 0;
public static void Main(string[] args)
{
Console.WriteLine("juster書(shū)的數(shù)量:" + bookNum);
Thread juster = new Thread(() =>
{
Console.WriteLine("juster沒(méi)帶書(shū),等待家長(zhǎng)送書(shū)到學(xué)校...");
while (bookNum == 0) { }
Console.WriteLine("juster拿到書(shū),開(kāi)始上課聽(tīng)講。");
});
juster.Name = nameof(juster);
juster.Start();
Thread parent = new Thread(() =>
{
Console.WriteLine("parent在屋里找書(shū)中...");
Thread.Sleep(2000);
Console.WriteLine("parent找到了書(shū)之后,送往學(xué)校...");
SendBook();
});
parent.Name = nameof(parent);
parent.Start();
}
public static void SendBook()
{
bookNum = 1;
}
}

在被各種優(yōu)化之后,booknum因?yàn)槭侵殿?lèi)型在每個(gè)線(xiàn)程訪(fǎng)問(wèn)時(shí)會(huì)發(fā)生復(fù)制且又是在靜態(tài)方法中被修改。所以每個(gè)線(xiàn)程都會(huì)復(fù)制booknum的值到當(dāng)前線(xiàn)程上下文中緩存起來(lái)。這樣就導(dǎo)致了parent線(xiàn)程修改了booknum的值juster線(xiàn)程看不到的情況。這個(gè)時(shí)候就需要用volatile關(guān)鍵字告訴編譯器不需要這樣的優(yōu)化,表示用volatile定義的變量會(huì)被改變,每次都必須從內(nèi)存中讀取,而不能把他放在CPU cache或寄存器中重復(fù)使用。最后booknum會(huì)在運(yùn)行的過(guò)程中修改值且其他線(xiàn)程能‘共享訪(fǎng)問(wèn)'達(dá)到最終的效果。
3.Conclusion
Part1
volatile 關(guān)鍵字可應(yīng)用于以下類(lèi)型的字段:
- 引用類(lèi)型。
- 指針類(lèi)型(在不安全的上下文中)。請(qǐng)注意,雖然指針本身可以是可變的,但是它指向的對(duì)象不能是可變的。換句話(huà)說(shuō),不能聲明“指向可變對(duì)象的指針”。
- 簡(jiǎn)單類(lèi)型,如
sbyte、byte、short、ushort、int、uint、char、float和bool。 - 具有以下基本類(lèi)型之一的
enum類(lèi)型:byte、sbyte、short、ushort、int或uint。 - 已知為引用類(lèi)型的泛型類(lèi)型參數(shù)。
- IntPtr 和 UIntPtr。
其他類(lèi)型(包括 double 和 long)無(wú)法標(biāo)記為 volatile,因?yàn)閷?duì)這些類(lèi)型的字段的讀取和寫(xiě)入不能保證是原子的。若要保護(hù)對(duì)這些類(lèi)型字段的多線(xiàn)程訪(fǎng)問(wèn),請(qǐng)使用 Interlocked 類(lèi)成員或使用 lock 語(yǔ)句保護(hù)訪(fǎng)問(wèn)權(quán)限。
volatile 關(guān)鍵字只能應(yīng)用于 class 或 struct 的字段。不能將局部變量聲明為 volatile。
Part2
volatile并不能用來(lái)做線(xiàn)程同步,它的主要作用時(shí)為了讓多個(gè)線(xiàn)程之間能看到被修改過(guò)后最新的值。

Part3
C#不支持以傳遞引用的方式將volatile字段傳給方法。
int.TryParse("123", out x);
Part4
除了禁止編譯優(yōu)化,還有同步到內(nèi)存中因?yàn)镃PU每個(gè)核心都有自己Cache所以需要同步到內(nèi)存中方便其他核心使用。
Part5
看完本文也能解開(kāi)小白時(shí)期的疑惑,為什么我寫(xiě)代碼編譯成release版本之后就不能運(yùn)行報(bào)錯(cuò)的奇特現(xiàn)象了。
Part6
volatile 牽扯到的相關(guān)知識(shí)點(diǎn)和原理遠(yuǎn)遠(yuǎn)不止這些。
4.Reference
到此這篇關(guān)于C# Volatile的具體使用的文章就介紹到這了,更多相關(guān)C# Volatile內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#使用winform簡(jiǎn)單導(dǎo)出Excel的方法
這篇文章主要介紹了C#使用winform簡(jiǎn)單導(dǎo)出Excel的方法,結(jié)合實(shí)例形式分析了WinForm操作Excel文件的寫(xiě)入導(dǎo)出等相關(guān)技巧,需要的朋友可以參考下2016-06-06
C#基于Socket的TCP通信實(shí)現(xiàn)聊天室案例
這篇文章主要為大家詳細(xì)介紹了C#基于Socket的TCP通信實(shí)現(xiàn)聊天室案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
C#怎樣實(shí)現(xiàn)文件下載斷點(diǎn)續(xù)傳
這篇文章主要介紹了C#怎樣實(shí)現(xiàn)文件下載斷點(diǎn)續(xù)傳,對(duì)斷點(diǎn)續(xù)傳感興趣的同學(xué),可以參考下2021-04-04
淺談C#各種數(shù)組直接的數(shù)據(jù)復(fù)制/轉(zhuǎn)換
下面小編就為大家?guī)?lái)一篇淺談C#各種數(shù)組直接的數(shù)據(jù)復(fù)制/轉(zhuǎn)換。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
C#實(shí)現(xiàn)接口base調(diào)用示例詳解
這篇文章主要為大家介紹了C#實(shí)現(xiàn)接口base調(diào)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
C#絕對(duì)路徑拼接相對(duì)路徑的實(shí)例代碼
C#絕對(duì)路徑拼接相對(duì)路徑的實(shí)例代碼,需要的朋友可以參考一下2013-03-03
C#中利用LINQ to XML與反射把任意類(lèi)型的泛型集合轉(zhuǎn)換成XML格式字符串的方法
本文主要介紹了C#中利用LINQ to XML與反射把任意類(lèi)型的泛型集合轉(zhuǎn)換成XML格式字符串的方法:利用反射,讀取一個(gè)類(lèi)型的所有屬性,然后再把屬性轉(zhuǎn)換成XML元素的屬性或者子元素。下面注釋比較完整,需要的朋友可以看下2016-12-12

