深入多線程之:內(nèi)存柵欄與volatile關(guān)鍵字的使用分析
以前我們說(shuō)過(guò)在一些簡(jiǎn)單的例子中,比如為一個(gè)字段賦值或遞增該字段,我們需要對(duì)線程進(jìn)行同步,
雖然lock可以滿足我們的需要,但是一個(gè)競(jìng)爭(zhēng)鎖一定會(huì)導(dǎo)致阻塞,然后忍受線程上下文切換和調(diào)度的開(kāi)銷,在一些高并發(fā)和性能比較關(guān)鍵的地方,這些是不能忍受的。
.net framework 提供了非阻塞同步構(gòu)造,為一些簡(jiǎn)單的操作提高了性能,它甚至都沒(méi)有阻塞,暫停,和等待線程。
Memory Barriers and Volatility (內(nèi)存柵欄和易失字段 )
考慮下下面的代碼:
int _answer;
bool _complete;
void A()
{
_answer = 123;
_complete = true;
}
void B()
{
if (_complete)
Console.WriteLine(_answer);
}
如果方法A和B都在不同的線程下并發(fā)的執(zhí)行,方法B可能輸出 “0” 嗎?
回答是“yes”,基于以下原因:
編譯器,clr 或 cpu 可能會(huì)為了性能而重新為程序的指令進(jìn)行排序,例如可能會(huì)將方法A中的兩句代碼的順序進(jìn)行調(diào)整。
編譯器,clr 或 cpu 可能會(huì)為變量的賦值采用緩存策略,這樣這些變量就不會(huì)立即對(duì)其他變量可見(jiàn)了,例如方法A中的變量賦值,不會(huì)立即刷新到內(nèi)存中,變量B看到的變量并不是最新的值。
C# 和運(yùn)行時(shí)非常小心的保證這些優(yōu)化策略不會(huì)影響正常的單線程的代碼和在多線程環(huán)境下加鎖的代碼。
除此之外,你必須顯示的通過(guò)創(chuàng)建內(nèi)存屏障(Memory fences) 來(lái)限制指令重新排序和讀寫緩存對(duì)程序造成的影響。
Full fences:
最簡(jiǎn)單的完全柵欄的方法莫過(guò)于使用Thread.MemoryBarrier方法了。
以下是msdn的解釋:
Thread.MemoryBarrier: 按如下方式同步內(nèi)存訪問(wèn):執(zhí)行當(dāng)前線程的處理器在對(duì)指令重新排序時(shí),不能采用先執(zhí)行 MemoryBarrier 調(diào)用之后的內(nèi)存訪問(wèn),再執(zhí)行 MemoryBarrier 調(diào)用之前的內(nèi)存訪問(wèn)的方式。
按照我個(gè)人的理解:就是寫完數(shù)據(jù)之后,調(diào)用MemoryBarrier,數(shù)據(jù)就會(huì)立即刷新,另外在讀取數(shù)據(jù)之前調(diào)用MemoryBarrier可以確保讀取的數(shù)據(jù)是最新的,并且處理器對(duì)MemoryBarrier的優(yōu)化小心處理。
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); //在寫完之后,創(chuàng)建內(nèi)存柵欄
_complete = true;
Thread.MemoryBarrier();//在寫完之后,創(chuàng)建內(nèi)存柵欄
}
void B()
{
Thread.MemoryBarrier();//在讀取之前,創(chuàng)建內(nèi)存柵欄
if (_complete)
{
Thread.MemoryBarrier();//在讀取之前,創(chuàng)建內(nèi)存柵欄
Console.WriteLine(_answer);
}
}
一個(gè)完全的柵欄在現(xiàn)代桌面應(yīng)用程序中,大于需要花費(fèi)10納秒。
下面的一些構(gòu)造都隱式的生成完全柵欄。
C# Lock 語(yǔ)句(Monitor.Enter / Monitor.Exit)
在Interlocked類的所有方法。
使用線程池的異步回調(diào),包括異步的委托,APM 回調(diào),和 Task continuations.
在一個(gè)信號(hào)構(gòu)造中的發(fā)送(Settings)和等待(waiting)
你不需要對(duì)每一個(gè)變量的讀寫都使用完全柵欄,假設(shè)你有三個(gè)answer 字段,我們?nèi)匀豢梢允褂?個(gè)柵欄。例如:
int _answer1, _answer2, _answer3;
bool _complete;
void A()
{
_answer1 = 1; _answer2 = 2; _answer3 = 3;
Thread.MemoryBarrier(); //在寫完之后,創(chuàng)建內(nèi)存柵欄
_complete = true;
Thread.MemoryBarrier(); //在寫完之后,創(chuàng)建內(nèi)存柵欄
}
void B()
{
Thread.MemoryBarrier(); //在讀取之前,創(chuàng)建內(nèi)存柵欄
if (_complete)
{
Thread.MemoryBarrier(); //在讀取之前,創(chuàng)建內(nèi)存柵欄
Console.WriteLine(_answer1 + _answer2 + _answer3);
}
}
我們真的需要lock 和內(nèi)存柵欄嗎?
在一個(gè)共享可寫的字段上不使用lock 或者柵欄 就是在自找麻煩,在msdn上有很多關(guān)于這方面的主題。
考慮下下面的代碼:
public static void Main()
{
bool complete = false;
var t = new Thread(() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join();
}
如果你在Visual Studio中選擇發(fā)布模式,生成該應(yīng)用程序,那么如果你直接運(yùn)行應(yīng)用程序,程序都不會(huì)中止。
因?yàn)镃PU 寄存器把 complete 變量的值給緩存了。在寄存器中,complete永遠(yuǎn)都是false。
通過(guò)在while循環(huán)中插入Thread.MemoryBarrier,或者是在讀取complete的時(shí)候加鎖 都可以解決這個(gè)問(wèn)題。
volatile 關(guān)鍵字
為_(kāi)complete字段加上volatile關(guān)鍵字也可以解決這個(gè)問(wèn)題。
volatile bool _complete.
Volatile關(guān)鍵字會(huì)指導(dǎo)編譯器自動(dòng)的為讀寫字段加屏障.以下是msdn的解釋:
volatile 關(guān)鍵字指示一個(gè)字段可以由多個(gè)同時(shí)執(zhí)行的線程修改。聲明為 volatile 的字段不受編譯器優(yōu)化(假定由單個(gè)線程訪問(wèn))的限制。這樣可以確保該字段在任何時(shí)間呈現(xiàn)的都是最新的值。
使用volatile字段可以被總結(jié)成下表:
|
第一條指令 |
第二條指令 |
可以被交換嗎? |
|
Read |
Read |
No |
|
Read |
Write |
No |
|
Write |
Write |
No(CLR會(huì)確保寫和寫的操作不被交換,甚至不使用volatile關(guān)鍵字) |
|
Write |
Read |
Yes! |
注意到應(yīng)用volatile關(guān)鍵字,并不能保證寫后面跟讀的操作不被交換,這有可能會(huì)造成莫名其妙的問(wèn)題。例如:
volatile int x, y;
void Test1()
{
x = 1; //Volatile write
int a = y; //Volatile Read
}
void Test2()
{
y = 1; //Volatile write
int b = x; //Volatile Read
}
如果Test1和Test2在不同的線程中并發(fā)執(zhí)行,有可能a 和b 字段的值都是0,(盡管在x和y上應(yīng)用了volatile 關(guān)鍵字)
這是一個(gè)避免使用volatile關(guān)鍵字的好例子,甚至假設(shè)你徹底的明白了這段代碼,是不是其他在你的代碼上工作的人也全部明白呢?。
在Test1 和Test2方法中使用完全柵欄或者是lock都可以解決這個(gè)問(wèn)題,
還有一個(gè)不使用volatile關(guān)鍵字的原因是性能問(wèn)題,因?yàn)槊看巫x寫都創(chuàng)建了內(nèi)存柵欄,例如
volatile m_amount
m_amount = m_amount + m_amount.
Volatile 關(guān)鍵字不支持引用傳遞的參數(shù),和局部變量。在這樣的場(chǎng)景下,你必須使用
VolatileRead和VolatileWrite方法。例如
volatile int m_amount;
Boolean success =int32.TryParse(“123”,out m_amount);
//生成如下警告信息:
//cs0420:對(duì)volatile字段的引用不被視為volatile.
VolatileRead 和VolatileWrite
從技術(shù)上講,Thread類的靜態(tài)方法VolatileRead和VolatileWrite在讀取一個(gè) 變量上和volatile 關(guān)鍵字的作用一致。
他們的實(shí)現(xiàn)是一樣是低效率的,盡管事實(shí)上他們都創(chuàng)建了內(nèi)存柵欄。下面是他們?cè)趇nteger類型上的實(shí)現(xiàn)。
public static void VolatileWrite(ref int address, int value)
{
Thread.MemoryBarrier(); address = value;
}
public static int VolatileRead(ref int address)
{
int num = address; Thread.MemoryBarrier(); return num;
}
你可以看到如果你在調(diào)用VolatileWrite之后調(diào)用VolatileRead,在中間沒(méi)有柵欄會(huì)被創(chuàng)建,這同樣會(huì)導(dǎo)致我們上面講到寫之后再讀順序可能變換的問(wèn)題。
相關(guān)文章
c#操作sql server2008 的界面實(shí)例代碼
這篇文章主要介紹了c#操作sql server2008 的界面實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03
C#中使用XmlDocument類來(lái)創(chuàng)建和修改XML格式的數(shù)據(jù)文件
這篇文章主要介紹了C#中使用XmlDocument類來(lái)創(chuàng)建和修改XML格式的數(shù)據(jù)文件的方法,XmlDocument類被包含在.NET框架中,需要的朋友可以參考下2016-04-04
基于.net中突破每客戶端兩個(gè)http連接限制的詳細(xì)介紹
本篇文章是對(duì).net中突破每客戶端兩個(gè)http連接限制進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
C#的FileSystemWatcher用法實(shí)例詳解
這篇文章主要介紹了C#的FileSystemWatcher用法,以實(shí)例形似詳細(xì)分析了FileSystemWatcher控件主要功能,并總結(jié)了FileSystemWatcher控件使用的技巧,需要的朋友可以參考下2014-11-11
Unity編輯器資源導(dǎo)入處理函數(shù)OnPreprocessAudio用法示例
這篇文章主要為大家介紹了Unity編輯器資源導(dǎo)入處理函數(shù)OnPreprocessAudio用法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08

