敵不動 我不動!
小明同學,上週未和家人出去遊玩去了。剛學了鎖的用法,小明終於完善的模擬出了早餐店的流水線,所以他遊玩很開心。但是回家的路上,卻遇到了煩心事!
由於天氣很好,小明一家人遊玩到了天黑才驅車回家。正值交通擁堵的時候,在他們即將行進到一個環島的時候,交通完全堵死了。
傳說中的堵死
小明在車上看著道路資源被無限的佔用著,聯想到多執行緒程式設計中的鎖:要是限制一下進入環島的車輛的數量,是不是就不會出現這種無限的堵死在狀態呢!
由於車輛太多,已經進入環島的車輛,出環島的路被堵死,無法出去,無法釋放佔用的道路資源。想要進入環島的車輛,卻又因為無法進入環島而又一直佔用著環島的出口。這不就是個死迴圈嘛!
在堵了一個小時之後,小明一家人終於走過了這個環島,不一會就到家了。
死鎖演示聰明的小明,直覺告訴他,在多執行緒中,也可能會出現這種現象。於是他用兩個鎖物件,模擬了環島堵死的場景:
class Program{ static long uniqueRes = 0; // 唯一資源 // 鎖物件 static object roadOut = new object(); static object roadIn = new object(); static void EnterCircle() { lock (roadIn) { // 模擬堵在了入口 for (int i = 0; i < 10000; i++) { uniqueRes += 1; } lock (roadOut) { // 模擬堵在了出口 for (int i = 0; i < 10000; i++) { uniqueRes -= 1; } } } } static void ExitCircle() { // 將資源鎖上 lock (roadOut) { // 模擬堵在了出口處 while (true) { uniqueRes -= 1; } } } static void Main(string[] args) { Console.WriteLine("Hello Thread Dead Locker!"); var t1 = new Thread(EnterCircle); var t2 = new Thread(ExitCircle); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine($"sum: {uniqueRes}"); Console.ReadKey(); }}
執行結果如下:
程式無法結束
果然程式也堵住了。
其實小明模擬的情形,是一種資源的惡意佔用。一直佔用資源而不歸還,導致其他執行緒無法訪問資源。這種情況,在實際程式設計中,不多見,除非是惡意程式碼。一般正常的程式碼,就算長時間佔用資源,最終都會歸還資源的。
但是有另外一種交叉佔用資源導致的死鎖問題,要特別小心。我把小明的程式碼做了一點修改:
class Program{ static long uniqueResA = 0; // 唯一資源 static long uniqueResB = 0; // 唯一資源 // 鎖物件 static object A = new object(); static object B = new object(); static void Fun1() { lock (A) { for (int i = 0; i < 10000; i++) { uniqueResA += 1; } lock (B) { for (int i = 0; i < 10000; i++) { uniqueResB -= 1; } } } } static void Fun2() { lock (B) { for (int i = 0; i < 10000; i++) { uniqueResB -= 1; } lock (A) { for (int i = 0; i < 10000; i++) { uniqueResA += 1; } } } } static void Main(string[] args) { Console.WriteLine("Hello Thread Dead Locker!"); var t1 = new Thread(Fun1); var t2 = new Thread(Fun2); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine($"A: {uniqueResA} B: {uniqueResB}"); Console.ReadKey(); }}
執行3次的結果如下:
資源交叉佔用可能導致死鎖
由於Fun1和Fun2幾乎同時執行,剛開始Fun1請求資源A成功,Fun2請求資源B也成功。但是當Fun1開始請求資源B的時候,如果Fun2還沒有釋放B,就會導致Fun1阻塞。Fun1阻塞又導致其無法歸還資源A,進而導致Fun2又無法請求到資源A,進而又導致Fun2阻塞。。。
你不讓我也沒法讓
小明看到這個結果,就舉手表示有疑問:為什麼第三次執行成功了呢?
我並沒有回答小明的問題,而是佈置了作業:
1.同學們回家之後,將Fun1和Fun2中的迴圈中的計算的次數調高和調低,然後觀察程式多次執行出現死鎖的機率。
2.如何避免出現死鎖現象?
3.如果出現死鎖,如何解除死鎖?
小明似乎明白了什麼,但是我立即示意他不要說出來!先驗證再說結論!
聰明的小明
附:參考答案
1.迴圈中,計算次數越多,出現死鎖的機率越高
2.a 儘量避免長時間佔用資源
2.b 避免資源交叉請求
3.a 有序資源分配法
3.b 銀行家演算法
踩坑記錄這次發現一個和此前我的理解不同的地方,分享給大家:
請問開啟4個執行緒,同時執行如下的DealRes函式,會導致死鎖嗎?
static long uniqueRes = 0; // 唯一資源// 鎖物件static object locker = new object();static void DealRes(){ // 將資源鎖上 lock (locker) { // 再來一次lock,會死鎖嗎?! lock (locker) { for (int i = 0; i < 10000; i++) { uniqueRes += 1; } } }}
下期預告下面是給同學們準備的乾貨,正在陸續發貨中哦:
Task
Parallel
await/async
Linq與PLinq (ParallelEnumerable)
。。。 。。。