【转】细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)
上一節(jié)介紹了使用信號(hào)量進(jìn)行同步,本節(jié)主要介紹一些非阻塞同步的方法。本節(jié)主要介紹MemoryBarrier,volatile,Interlocked。
MemoryBarriers
本文簡(jiǎn)單的介紹一下這兩個(gè)概念,假設(shè)下面的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using?System; class?Foo { ????int?_answer; ????bool?_complete; ? ????void?A() ????{ ????????_answer = 123; ????????_complete =?true; ????} ? ????void?B() ????{ ????????if?(_complete) Console.WriteLine(_answer); ????} } |
如果方法A和方法B同時(shí)在兩個(gè)不同線程中運(yùn)行,控制臺(tái)可能輸出0嗎?答案是可能的,有以下兩個(gè)原因:
- 編譯器,CLR或者CPU可能會(huì)更改指令的順序來(lái)提高性能
- 編譯器,CLR或者CPU可能會(huì)通過(guò)緩存來(lái)優(yōu)化變量,這種情況下對(duì)其他線程是不可見(jiàn)的。
最簡(jiǎn)單的方式就是通過(guò)MemoryBarrier來(lái)保護(hù)變量,來(lái)防止任何形式的更改指令順序或者緩存。調(diào)用Thread.MemoryBarrier會(huì)生成一個(gè)內(nèi)存柵欄,我們可以通過(guò)以下的方式解決上面的問(wèn)題:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using?System; using?System.Threading; class?Foo { ????int?_answer; ????bool?_complete; ? ????void?A() ????{ ????????_answer = 123; ????????Thread.MemoryBarrier();????// Barrier 1 ????????_complete =?true; ????????Thread.MemoryBarrier();????// Barrier 2 ????} ? ????void?B() ????{ ????????Thread.MemoryBarrier();????// Barrier 3 ????????if?(_complete) ????????{ ????????????Thread.MemoryBarrier();???????// Barrier 4 ????????????Console.WriteLine(_answer); ????????} ????} } |
上面的例子中,barrier1和barrier3用來(lái)保證指令順序不會(huì)改變,barrier2和barrier4用來(lái)保證值變化不被緩存。一個(gè)好的處理方案就是我們?cè)谛枰Wo(hù)的變量前后分別加上MemoryBarrier。
在c#中,下面的操作都會(huì)生成MemoryBarrier:
- Lock語(yǔ)句(Monitor.Enter,Monitor.Exit)
- 所有Interlocked類(lèi)的方法
- 線程池的回調(diào)方法
- Set或者Wait信號(hào)
- 所有依賴(lài)于信號(hào)燈實(shí)現(xiàn)的方法,如starting或waiting 一個(gè)Task
因?yàn)樯厦孢@些行為,這段代碼實(shí)際上是線程安全的:
| 1 2 3 4 | int?x = 0; Task t = Task.Factory.StartNew(() => x++); t.Wait(); Console.WriteLine(x);????// 1 |
在你自己的程序中,你可能重現(xiàn)不出來(lái)上面例子所說(shuō)的情況。事實(shí)上,從msdn上對(duì)MomoryBarrier的解釋來(lái)看,只有對(duì)順序保護(hù)比較弱的多核系統(tǒng)才需要用到MomoryBarrier。但是有一點(diǎn)需要注意:多線程去修改變量并且不使用任何形式的鎖或者內(nèi)存柵欄是會(huì)帶來(lái)一定的麻煩的。
下面一個(gè)例子能夠很好的說(shuō)明上面的觀點(diǎn)(在你的VisualStudio中,選擇Release模式,并且Start Without Debugging重現(xiàn)這個(gè)問(wèn)題):
| 1 2 3 4 5 6 7 8 9 10 | bool?complete =?false; var?t =?new?Thread(() => { ????bool?toggle =?false; ????while?(!complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); complete =?true; t.Join();????????// Blocks indefinitely |
這個(gè)程序永遠(yuǎn)不會(huì)結(jié)束,因?yàn)閏omplete變量被緩存在了CPU寄存器中。在while循環(huán)中加入Thread.MemoryBarrier可以解決這個(gè)問(wèn)題。
volatile關(guān)鍵字
另外一種更高級(jí)的方式來(lái)解決上面的問(wèn)題,那就是考慮使用volatile關(guān)鍵字。Volatile關(guān)鍵字告訴編譯器在每一次讀操作時(shí)生成一個(gè)fence,來(lái)實(shí)現(xiàn)保護(hù)保護(hù)變量的目的。具體說(shuō)明可以參見(jiàn)msdn的介紹
VolatileRead和VolatileWrite
Volatile關(guān)鍵字只能加到類(lèi)變量中。本地變量不能被聲明成volatile。這種情況你可以考慮使用System.Threading.Volatile.Read方法。我們看一下System.Threading.Volatile源碼如何實(shí)現(xiàn)這兩個(gè)方法的:
| 1 2 3 4 5 6 7 8 9 10 11 | public?static?bool?Read(ref?bool?location) { ????bool?flag = location; ????Thread.MemoryBarrier(); ????return?flag; } public?static?void?Write(ref?bool?location,?bool?value) { ????Thread.MemoryBarrier(); ????location = value; } |
一目了然,通過(guò)MemoryBarrier來(lái)實(shí)現(xiàn)的,但是他只在讀操作的后面和寫(xiě)操作的前面加了MemoryBarrier,那么你應(yīng)該考慮,如果你先使用Volatile.Write再使用Volatile.Read是不是可能有問(wèn)題呢?
c#中ConcurrentDictionary中使用了Volatile類(lèi)來(lái)保護(hù)變量,有興趣的讀者可以看看c#的開(kāi)發(fā)者是如何使用這個(gè)方法來(lái)保護(hù)變量的。
Interlocked
使用MemoryBarrier并不總是一個(gè)好的解決方案,尤其在不需要鎖的情況下。Interlocked方法提供了一些常用的原子操作來(lái)避免前面文章提到的一系列的問(wèn)題。如使用Interlocked.Increment來(lái)替代++,Interlocked.Decrement來(lái)替代--。Msdn的文檔中詳細(xì)的介紹了相關(guān)的用法和原理。C#中的源碼里也經(jīng)常能看見(jiàn)Interlocked相關(guān)的使用。
?
本文介紹了一些除了鎖和信號(hào)量之外的一些同步方式,歡迎批評(píng)與指正。
總結(jié)
以上是生活随笔為你收集整理的【转】细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 618还有4天结束 Redmi成安卓阵营
- 下一篇: AMD锐龙、显卡越来越贵:毛利率将超越I