死鎖是很討厭的(雖然活鎖更討厭),如何避免死鎖呢?
在兩個線程間的循環等待是比較容易識別的,但是在死 鎖的形成中如果包含多個線程,那麼就是難以發現的(現實中不少這種情況)。
首先來看看死鎖形成的幾個必要條 件
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;
}
}
鎖分級的復雜性:
如果要為鎖指定級別,那麼就需要規劃並遵守一定的原則。我們很難從一開始就 提出很完備的鎖分級策略。也比較難估計哪些地方使用到鎖。