死鎖是很討厭的(雖然活鎖更討厭),如何避免死鎖呢?
在兩個線程間的循環等待是比較容易識別的,但是在死 鎖的形成中如果包含多個線程,那麼就是難以發現的(現實中不少這種情況)。
首先來看看死鎖形成的幾個必要條 件
1、互斥
2、等待
3、不可搶占
4、循環等待
除了完全避免多線程編程之外,如果要 避免死鎖,那麼必須要使得上面這4個條件中有任意一個不滿足。
1、互斥是大多數鎖的一種固有性質,你沒辦法改 變它。
2、如果程序持有的鎖不會多於一個,那麼就不會發生死鎖問題。但是這通常也是不可能的。
3、不可 搶占,線程中斷和線程終止並非是實現這個功能的合適方式。
4、循環等待。通過在鎖上施加一種順序,並且要求線 程按照這種順序來獲取鎖。那麼就不會發生循環的獲取鎖操作,也就不會發生死鎖。這也是這四種條件中最容易消除的。
我們用一個簡單的例子實現簡單的鎖分級。
第一種會有並發問題的寫法,我們本意是通過形參fromAccount , toAccount 來表明鎖的順序,可這是做不到的。因此如果我們調換了一下傳入的實參的順序,就會產生死鎖問題。
class BankAccount { public int id { get; set; } public decimal Balance { get; set; } public static void Transfer(BankAccount fromAccount, BankAccount toAccount, decimal amount) { lock (fromAccount) { if (fromAccount.Balance < amount) { throw new Exception("Insufficient funds"); } lock (toAccount) { fromAccount.Balance -= amount; toAccount.Balance += amount; } } } }
我們可以通過比較id來保證順序,同時這種寫法不會引入新的開銷。但是這種寫法局限性比較大,不通用。
class BankAccount { public int id { get; set; } public decimal Balance { get; set; } public static void SafeTransfer(BankAccount fromAccount, BankAccount toAccount, decimal amount) { if (fromAccount.id < toAccount.id) { lock (fromAccount) { lock (toAccount) { transferMoney(fromAccount, toAccount, amount); } } } else if (fromAccount.id > toAccount.id) { lock (toAccount) { lock (fromAccount) { TransferMoney(fromAccount, toAccount, amount); } } } } private static void TransferMoney(BankAccount fromAccount, BankAccount toAccount, decimal amount) { if (fromAccount.Balance < amount) { throw new Exception("Insufficient funds"); } fromAccount.Balance -= amount; toAccount.Balance += amount; } }
本質上是通過固定一種順序,因此我們想到可以通過排序的方式使得可以接受多個鎖,並且更通用。
class MultiLocksHelper<T> where T :IComparable<T> { internal static void Enter(params T[] locks) { Array.Sort(locks); int i = 0; try { for (; i < locks.Length; i++) { Monitor.Enter(locks[i]); } } catch { for(int j= i-1;j>0;j--) { Monitor.Exit(locks[j]); } throw; } } internal static void Exit(params T[] locks) { Array.Sort(locks); for (int i = locks.Length - 1; i >= 0; i--) { Monitor.Exit(locks[i]); } } }
這時調用起來就很簡單了,只有幾行代碼。
class BankAccount : IComparable<BankAccount> { public int id { get; set; } public decimal Balance { get; set; } public static void GenericSafeTransfer(BankAccount fromAccount, BankAccount toAccount, decimal amount) { MultiLocksHelper<BankAccount>.Enter(fromAccount, toAccount); try { TransferMoney(fromAccount, toAccount, amount); } finally { MultiLocksHelper<BankAccount>.Exit(fromAccount, toAccount); } } private static void TransferMoney(BankAccount fromAccount, BankAccount toAccount, decimal amount) { if (fromAccount.Balance < amount) { throw new Exception("Insufficient funds"); } fromAccount.Balance -= amount; toAccount.Balance += amount; } public int CompareTo(BankAccount other) { if (this.id > other.id) return 1; else return -1; } }
鎖分級的復雜性:
如果要為鎖指定級別,那麼就需要規劃並遵守一定的原則。我們很難從一開始就 提出很完備的鎖分級策略。也比較難估計哪些地方使用到鎖。