標籤:ref 無法 some 靜態類 cpu length 針對 代碼 add
建議89:在並行方法體中謹慎使用鎖
除了建議88所提到的場合,要謹慎使用並行的情況還包括:某些本身就需要同步啟動並執行場合,或者需要較長時間鎖定共用資源的場合。
在對整型資料進行同步操作時,可以使用靜態類Interlocked的Add方法,這就極大地避免了由於進行原子操作長時間鎖定某個共用資源所帶來的同步效能損耗。回顧建議83中的例子。
static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total = 0; Parallel.For<int>(0, nums.Length, () => { return 1; }, (i, loopState, subtotal) => { subtotal += nums[i]; return subtotal; }, (x) => Interlocked.Add(ref total, x) ); Console.WriteLine("total={0}", total); Console.ReadKey(); }
理論上,針對total的加法操作,需要使用一個同步鎖,否則就無法避免一次torn read(即兩次mov操作所導致的欄位記憶體位址邊界對齊問題)。FCL通過提供Interlocked類型解決了這個問題。FCL用來解決簡單類型的原子性操作還提供了volatile關鍵字。不過這些都不是本建議所要討論的重點。FCL現有的原子性操作為我們同步整型資料的時候,帶來了效能上的提高。但是,在其他一些場合,我們卻不得不考慮因為同步鎖帶來的損耗。
來看一個例子:
static void Main(string[] args) { SampleClass sample = new SampleClass(); Parallel.For(0, 10000000, (i) => { sample.SimpleAdd(); }); Console.WriteLine(sample.SomeCount); } class SampleClass { public long SomeCount { get; private set; } public void SimpleAdd() { SomeCount++; } }
這段代碼的輸出或許是:
8322580
顯然,這與我們的期待輸出10000000有很大的差距。為了保證輸出正確,必須為並行中的方法體加鎖(假設SampleClass是外部提供的API,無權進行源碼修改在其內部加鎖):
object syncObj = new object(); Parallel.For(0, 10000000, (i) =>{ lock (syncObj) { sample.SimpleAdd(); } });
經過以上修改後,代碼輸出就正確了。但是,這段代碼也帶來了另外的問題。由於鎖的存在,系統的開銷也增加了,同步帶來的線程環境切換,使我們犧牲了CPU時間與空間效能。簡單地說,就是這段代碼還不如不用並行。在建議73中曾經提到過,鎖其實就是讓多線程變成單線程(因為同時只允許有一個線程訪問資源)。所以,我們需要謹慎地對待並行方法中的同步問題。如果方法體的全部內容都需要同步運行,就完全不應該使用並行。
轉自:《編寫高品質代碼改善C#程式的157個建議》陸敏技
【轉】編寫高品質代碼改善C#程式的157個建議——建議89:在並行方法體中謹慎使用鎖