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();
}
}
|
白話併發衝突與線程同步(2)——Monitor、lock和死鎖
1-2-3 和比爾蓋茨的一些往事
在上一篇裏我們說道,1-2-3寫了一段程序,並且在使用了2個線程分別執行foo1()和foo2()之後,程序的結果就不對了。
究其原因,就是因爲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.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.