白話併發衝突與線程同步(2)——Monitor、lock和死鎖

1-2-3 和比爾蓋茨的一些往事 在上一篇裏我們說道,1-2-3寫了一段程序,並且在使用了2個線程分別執行foo1()和foo2()之後,程序的結果就不對了。
class Program {     static int n = 0;     static void foo1()     {         for (int i = 0; i < 1000000000; i++// 10 浜?/span>         {                 int a = n;                 n = a + 1;         }         Console.WriteLine("foo1() complete n = {0}", n);     }     static void foo2()     {         for (int j = 0; j < 1000000000; j++// 10 浜?/span>         {                 int a = n;                 n = a + 1;         }         Console.WriteLine("foo2() complete n = {0}", n);     }     static void Main(string[] args)     {         new Thread(foo1).Start();         new Thread(foo2).Start();     } }
究其原因,就是因爲Windows總是不問青紅皁白隨隨便便就把我的線程給停掉了。例如,上面的那個程序很可能會以下面的順序來執行(黃色底色的代碼屬於第一個線程,綠色底色的代碼屬於第二個線程): 這樣,第一、第二個線程裏面的循環各自執行了3次,n的值是3,而不是我們期望的6。 所以呢,我就打算建議比爾蓋茨在C#里加一個關鍵字: 對foo2()也做同樣的修改,這樣,就可以確保程序以下圖所示的順序執行了: 如果這個建議被微軟接受,它將創造兩個記錄:   1. 它將是C#裏面第一個中文關鍵字。   2. 它將是C#裏面最長的關鍵字。 可是,比爾蓋茨聽了我的建議之後,卻把眉毛皺成了個大疙瘩,嘆道:“大哥,不行呀。你知道,Windows裏會同時運行着上千個線程,且不說那些居心不良的病毒和木馬,就是那些幹正經事的線程,誰又能保證在你那個超長關鍵字裏包裹的代碼不會運行個二、三十秒?CPU可只有一個,在那個線程運行的二、三十秒裏,整個Windows都會一動不動的,不知情的用戶還以爲是Windows又掛掉了,最後捱罵的可是兄弟我呦!” “不過,”比爾又接着說,“我可以提供另一種方案來達到同樣的效果。我可以讓線程1裏面的指定代碼塊不執行完,線程2就一直處於阻塞(ThreadState.WaitSleepJoin)狀態。” 要達到這個效果,需要使用.net裏的兩個函數。 Monitor.Enter(n); // 嘗試獲取對n的控制權。如果n沒主兒,則成功獲取了n的控制權;如果n已經有主兒了,則此線程阻塞,死等。 Monitor.Exit(n); // 釋放對n的控制權。等待着n的那個阻塞中的線程將獲取n的控制權,並從阻塞狀態變成運行狀態。 可以把n想像成WC裏的一個蹲位,線程1 Enter了之後,其它線程就不能Enter了,只能乾等着,直到線程1 Exit,下一個等着的線程才能Enter,之後才能繼續辦事。如果一個線程Enter了之後遲遲不Exit(例如Enter了之後,發生了異常,比如忘了帶SZ),就是所謂的“佔着MK不LS”了。(一邊吃午飯一邊看貼的兄弟對不住啦~~) 使用 Monitor 現在就可以在我的代碼裏使用Monitor了。
class Program {     static int n = 0;     static void foo1()     {         for (int i = 0; i < 1000000000; i++// 10 億         {             Monitor.Enter(n);             int a = n;             n = a + 1;             Monitor.Exit(n);         }         Console.WriteLine("foo1() complete n = {0}", n);     }     static void foo2()     {         for (int j = 0; j < 1000000000; j++// 10 億         {             Monitor.Enter(n);             int a = n;             n = a + 1;             Monitor.Exit(n);         }         Console.WriteLine("foo2() complete n = {0}", n);     }     static void Main(string[] args)     {         new Thread(foo1).Start();         new Thread(foo2).Start();     } }
這段代碼很可能會以下圖所示的順序執行(黃色底色的代碼屬於線程1,綠色底色的代碼屬於線程2。下圖演示了線程1循環2次,線程2循環1次,n的值爲3): 如果我們把上圖之中與Monitor相關的行和演示線程狀態的行去掉,就可以得到下圖: 怎麼樣?和我的那個超長關鍵字的效果一樣吧? 不過,如果你嘗試運行上面那個代碼,就會發現它根本無法通過編譯!這是因爲Monitor.Enter()只接受類型爲Object的參數。那麼,可不可以寫 Monitor.Enter((Object)n); 呢?它確實能夠通過編譯,但是這樣豈不是要裝箱20億次?所以千萬別這麼寫。沒法子了,我們只能再聲明一個Object類型的變量,專門用於這兩個線程的同步。
class Program {     static int n = 0;     static object mk = new object();     static void foo1()     {         for (int i = 0; i < 1000000000; i++// 10 億         {             Monitor.Enter(mk);             int a = n;             n = a + 1;             Monitor.Exit(mk);         }         Console.WriteLine("foo1() complete n = {0}", n);     }     static void foo2()     {         for (int j = 0; j < 1000000000; j++// 10 億         {             Monitor.Enter(mk);             int a = n;             n = a + 1;             Monitor.Exit(mk);         }         Console.WriteLine("foo2() complete n = {0}", n);     }     static void Main(string[] args)     {         new Thread(foo1).Start();         new Thread(foo2).Start();     } }
這段代碼在我的賽揚800的機器上運行時間爲3分零6秒。 lock 關鍵字 在C#裏面有一個lock關鍵字,它其實是一個語法糖。 小貼士:在VB裏與lock等價的關鍵字是SyncLock。用法是
SyncLock (mk)     Dim a As Integer = n     n = a + 1 End SyncLock
死鎖 還有比佔着MK不LS更惡劣的行徑麼?有,那就是吃着碗裏的望着鍋裏的。在下面的這段代碼中,線程1喜歡先佔着mk1然後在mk2裏辦事;線程2呢,喜歡先佔着mk2,然後在mk1裏辦事,要是這兩個活寶碰到一起……
class Program {     static object mk1 = new object();     static object mk2 = new object();     static void foo1()     {         for (int i = 0; i < 100; i++)         {             Monitor.Enter(mk1);             Console.WriteLine("i={0} 線程1:/"先佔着mk1,再去mk2裏辦事。/"", i);             Monitor.Enter(mk2);             Console.WriteLine("i={0} 線程1:/"進入了mk2,辦事/"", i);             Monitor.Exit(mk2);             Console.WriteLine("i={0} 線程1:/"辦完事了,離開mk2/"", i);             Monitor.Exit(mk1);             Console.WriteLine("i={0} 線程1:/"辦完事了,離開mk1/"", i);         }     }     static void foo2()     {         for (int j = 0; j < 100; j++)         {             Monitor.Enter(mk2);             Console.WriteLine("j={0} 線程2:/"先佔着mk2,再去mk1裏辦事。/"", j);             Monitor.Enter(mk1);             Console.WriteLine("j={0} 線程2:/"進入了mk1,辦事/"", j);             Monitor.Exit(mk1);             Console.WriteLine("j={0} 線程2:/"辦完事了,離開mk1/"", j);             Monitor.Exit(mk2);             Console.WriteLine("j={0} 線程2:/"辦完事了,離開mk2/"", j);         }     }     static void Main(string[] args)     {         new Thread(foo1).Start();         new Thread(foo2).Start();     } }
運行這段代碼,可以得到這樣的結果: 如上圖所示,當程序恰巧以“線程1 Enter mk1 -> 線程2 Enter mk2 -> 線程1 想要Enter mk2 發現 mk2 已經被佔用,線程1阻塞 -> 線程2 想要Enter mk1 發現 mk1 己經被佔用,線程2阻塞”這個順序執行時,線程1等待線程2釋放mk2,線程2等待線程1釋放mk1,兩個線程雙雙陷入阻塞狀態,直到山無棱、天地合……這就是死鎖。 參考文獻 Jeffrey Richter, CLR via C#, Second Edition. Microsoft Press, 2006.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章