簡單的程式也可以存在很多值得思考的地方,作為一名程式員或者架構師,首先要具備的就是追根和追新的心态。抓蟲系列的代碼我想大部分人都接觸過或者犯過這樣的錯誤,有些可能涉及的知識面很基礎很淺,留個爛文在此引導新手、路人。蟲子盡量将問題放大,追的深一點偏一點,如果大家有其他自己的想法或者補充也可以留爪印。
PS一下:看了下面的評論,大家有點誤會蟲子的意思了,此系列的博文旨在抓蟲,從抓蟲中關注我們的程式中容易出現的問題而并非是找尋更佳的解決方案 嘿嘿~ ~
先看原始bug程式
<a href="http://blog.51cto.com/dubing/712447#">?</a>
1
2
3
4
5
<code>class</code> <code>testObj</code>
<code> </code><code>{ </code>
<code> </code><code>public</code> <code>object</code> <code>Result { </code><code>get</code><code>; </code><code>set</code><code>; }</code>
<code> </code><code>public</code> <code>int</code> <code>index { </code><code>get</code><code>; </code><code>set</code><code>; } </code>
<code> </code><code>}</code>
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<code>public</code> <code>void</code> <code>Test()</code>
<code> </code><code>{</code>
<code> </code><code>ManualResetEvent[] MR = </code><code>new</code> <code>ManualResetEvent[20];</code>
<code> </code><code>testObj qq = </code><code>new</code> <code>testObj();</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < 20; i++)</code>
<code> </code><code>{</code>
<code> </code><code>MR[i] = </code><code>new</code> <code>ManualResetEvent(</code><code>false</code><code>);</code>
<code> </code><code>qq.index = i;</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o =></code>
<code> </code><code>{</code>
<code> </code><code>Console.WriteLine(qq.index.ToString());</code>
<code> </code><code>MR[qq.index].Set();</code>
<code> </code><code>}, qq);</code>
<code> </code><code>}</code>
<code> </code><code>WaitHandle.WaitAll(MR);</code>
<code> </code><code>}</code>
我們的目的是讓程式輸出0~19。看到這裡可能老鳥已經發現程式的問題了。新鳥應該還是繼續查蟲。老鳥們先賣個關子,蟲子把問題引偏,這樣好拓展更多的問題。讓我們來看看運作結果。
當時我就震驚了... 蟲子開始胡思亂想了 為嘛會是這樣。堆棧問題?好吧鞏固一下堆棧,值類型是放在棧中的,值類型的拷貝是深拷貝,當我們傳遞一個值類型參數時,棧上被配置設定好一個新的空間,然後該參數的值被拷貝到此空間中。應該不會産生這樣的現象吧。 不對!!! int已經被封裝在testObj裡了,這個地方它是引用類型是淺拷貝。Oh,MyGirlFriend,發現了!這20個線程用的是同一個副本,(⊙o⊙)… 蟲子在幹什麼,你讓20個線程在同時操作同一個資料。
這是個問題,。上面所說是一個問題但是不是這個現象産生的唯一問題。在這個異步程式中,線程在擷取對象資源時,那for循環再主線程中已經跑完了。是以qq的index一直是20。讀到這裡可能有些老鳥們已經開始嗤之以鼻,這種錯誤他們可不會犯。知道了原因我們就要來分析解決方案了。一層一層來剝,
首先看這段修補01号程式
20
<code> </code><code>{</code>
<code> </code><code>ManualResetEvent[] MR = </code><code>new</code> <code>ManualResetEvent[20];</code>
<code> </code><code>EventWaitHandle EH = </code><code>new</code> <code>AutoResetEvent(</code><code>false</code><code>);</code>
<code> </code><code>testObj qq = </code><code>new</code> <code>testObj();</code>
<code> </code><code>EH.Set();</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < 20; i++)</code>
<code> </code><code>{</code>
<code> </code><code>MR[i] = </code><code>new</code> <code>ManualResetEvent(</code><code>false</code><code>);</code>
<code> </code><code>qq.index = i;</code>
<code> </code><code>EH.WaitOne();</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o =></code>
<code> </code><code>{</code>
<code> </code><code>Console.WriteLine(qq.index.ToString());</code>
<code> </code><code>MR[qq.index].Set();</code>
<code> </code><code>EH.Set();</code>
<code> </code><code>}, qq);</code>
<code> </code><code>}</code>
<code> </code><code>WaitHandle.WaitAll(MR);</code>
<code> </code><code>}</code>
我們加了AutoResetEvent,這是一個自動Reset的事件通知方式。形象點說,相當于各位經常使用的門禁系統。一開始處于wait狀态,隻有有人set了它才放行。這裡用來控制線程一個一個來完成,也就是說每次對象隻有一個人能通路。這下咱們的線程安全了吧,哈哈哈哈。再看效果圖。
神馬!!! 貌似看好多了,居然還有重複的看那1、1,19、19這是多麼的不和諧啊。好吧,繼續抓蟲、(⊙o⊙)…又發現了 主線程雖然被門禁控制住了,但是主線程和異步線程的節奏還是不一樣,運氣好點可能你出的結果是正确的。但是主線程在第一次的時候可能已經pass掉了 但是第一個異步線程還沒結束。(⊙o⊙)… 蟲子你騙我們 這個方案根本不是解決這個問題的,我們的問題在于我們的程式用了同一個資源qq。
被你發現了!!!
好吧修補02号程式如下
<code> </code><code>qq = </code><code>new</code> <code>testObj();</code>
<code> </code><code>WaitHandle.WaitAll(MR); </code>
長歎一口氣,這下沒問題了吧。嘿嘿,這次我可以為每個線程獨立配置設定了一個對象。事實如此嗎?老鳥們應該開始偷着笑了,看效果圖
╮(╯▽╰)╭ 你又被忽悠了,這個問題關鍵的地方在第四行,貌似你每個形成用的都是獨立的資源,其實你還是粗心了點。
下面上正解方案了
<code>public</code> <code>class</code> <code>cnBlog</code>
<code> </code><code>{</code>
<code> </code><code>public</code> <code>void</code> <code>Test()</code>
<code> </code><code>testObj qq = </code><code>new</code> <code>testObj();</code>
<code> </code><code>WaitHandle.WaitAll(MR); </code>
<code> </code><code>}</code>
和之前的方案隻是将一行程式換了個地方,但是意義完全改變了,讓我們看看真正的結果是如何。
oh yeah~ ~ 雖然排序不理想 正常!
題後話就不多說了,希望抓蟲能給大家帶來一些微不足道的收獲。
本文轉自 熬夜的蟲子 51CTO部落格,原文連結:http://blog.51cto.com/dubing/712447