在前面四节中,我们一直没有讨论多线程程序的一个负面问题——死锁,有了一定的基础,现在是时候研究一下死锁了。死锁一定是出现在多线程程序中,单线程是不可能造成死锁的,因为你不可能同时加两把锁。死锁有个简单的例子,假设你和你的邻居关系很好,你们相互放了一把备用钥匙在对方家里,一般来说,只要有一个人带了钥匙,那两个人一定可以各回各家,但是恰好有一天,两人出门都忘记带钥匙,回家时发现对方在门口等着,两个人只能干瞪眼。这就是死锁。可以用下面这个图解释:
如果满足以下条件,则可能会发生死锁:
互斥:这意味着在特定时间只有一个线程可以访问资源。
保持并等待:这是一种情况,其中一个线程至少持有一个资源并等待另一个线程已经获取的至少一个资源。
无抢占:如果一个线程已经获得了一个资源,在它自愿放弃对该资源的控制之前,不能将其从该线程中夺走。
循环等待:这是两个或多个线程正在等待链中下一个成员获取的资源的情况。
1.死锁举例
为了更好的解释死锁,以及寻找避免死锁的办法,我们还是用程序说话。这里举一个账户转账的例子
首先有一个账户类:
namespace DeadLockDemo
{
public class Account
{
public int ID { get; }
private double Balance { get; set;}
public Account(int id, double balance)
{
ID = id;
Balance = balance;
}
public void WithdrawMoney(double amount)
{
Balance -= amount;
}
public void DepositMoney(double amount)
{
Balance += amount;
}
}
}
然后有一个账户经理类:
using System;
using System.Threading;
namespace DeadLockDemo
{
public class AccountManager
{
private Account FromAccount;
private Account ToAccount;
private double TransferAmount;
public AccountManager(Account AccountFrom, Account AccountTo, double AmountTransfer)
{
FromAccount = AccountFrom;
ToAccount = AccountTo;
TransferAmount = AmountTransfer;
}
public void FundTransfer()
{
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {FromAccount.ID}");
lock (FromAccount)
{
Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {FromAccount.ID}");
Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
Thread.Sleep(1000);
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {ToAccount.ID}");
lock (ToAccount)
{
FromAccount.WithdrawMoney(TransferAmount);
ToAccount.DepositMoney(TransferAmount);
}
}
}
}
}
因为在转账的时候,为了避免数据不一致,我们分别要锁定扣款账户,以及首款账户,否则,假设在这个过程刚好家人用这个首款账户支付一笔消费,那就会导致账户首款后数额不对。好了我们已经做好准备工作了,我们在main函数中实现转账逻辑:
using System;
using System.Threading;
namespace DeadLockDemo
{
class Program
{
public static void Main()
{
Console.WriteLine("Main Thread Started");
Account Account1001 = new Account(1001, 5000);
Account Account1002 = new Account(1002, 3000);
AccountManager accountManager1 = new AccountManager(Account1001, Account1002, 5000);
Thread thread1 = new Thread(accountManager1.FundTransfer)
{
Name = "Thread1"
};
AccountManager accountManager2 = new AccountManager(Account1002, Account1001, 6000);
Thread thread2 = new Thread(accountManager2.FundTransfer)
{
Name = "Thread2"
};
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Main Thread Completed");
Console.ReadKey();
}
}
}
运行一下:
Main Thread Started
Thread2 trying to acquire lock on 1002
Thread2 acquired lock on 1002
Thread2 Doing Some work
Thread1 trying to acquire lock on 1001
Thread1 acquired lock on 1001
Thread1 Doing Some work
Thread1 trying to acquire lock on 1002
Thread2 trying to acquire lock on 1001
注意:上面显然不是运行结束,而是出现死锁,卡住了。死锁的原因很简单,两个账户同事向对方转账,导致两个账户都被锁住,无法访问对方对象。
那怎么解决这个问题呢?
好问题!
2.解决死锁
我们在介绍前面Monitor类,提到过一个函数TryEnter(object obj,int milisecondsTimeout)。使用这个函数,我们可以指定线程释放锁的超时时间,如果一个线程长时间持有一个资源,而另一个线程正在等待,那么Monitor会提供一个时间限制,强制释放锁。这样其他线程就会进入临界区。 也就是必须让一个线程拖鞋,有点像两辆车向西在独木桥上,如果两个车都不愿意退让,那么就会一直卡在独木桥上,必须有一个车先退出到桥外,方可通行。修改后的代码如下:
internal class AccountManager
{
private readonly Account FromAccount;
private readonly Account ToAccount;
private double TransferAmount;
private readonly int WaittingTime;
public AccountManager(Account accountFrom,Account accountTo, double accountTransfer, int waittingTime)
{
FromAccount = accountFrom;
ToAccount = accountTo;
TransferAmount = accountTransfer;
WaittingTime = waittingTime;
}
public void Transfer()
{
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {FromAccount.ID}");
lock (FromAccount)
{
Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {FromAccount.ID}");
Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
Thread.Sleep(1000);
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {ToAccount.ID}");
if(Monitor.TryEnter(ToAccount,WaittingTime))
{
Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {ToAccount.ID}");
try
{
FromAccount.WithdrawMoney(TransferAmount);
ToAccount.DepositMoney(TransferAmount);
}
finally
{
Monitor.Exit(ToAccount);
}
}
else
{
Console.WriteLine($"{Thread.CurrentThread.Name} Unable to acquire lock on {ToAccount.ID}, So 终止交易.");
}
//lock (ToAccount)
//{
// FromAccount.WithdrawMoney(TransferAmount);
// ToAccount.DepositMoney(TransferAmount);
//}
}
}
}
然后运行结果如下:
Main Thread Started
Thread1 trying to acquire lock on 1001
Thread1 acquired lock on 1001
Thread1 Doing Some work
Thread2 trying to acquire lock on 1002
Thread2 acquired lock on 1002
Thread2 Doing Some work
Thread1 trying to acquire lock on 1002
Thread2 trying to acquire lock on 1001
Thread1 Unable to acquire lock on 1002, So 终止交易.
Thread2 acquired lock on 1001
Main Thread Completed
这里要说明的是,这个方案并不完美,读者应该可以发现我增加了一个等待时间属性,就是为了让两个经理类等待时间不一样,否则会出现两个转账在等待相同时间后,同时终止交易。
另一种解决方案是,两个线程在获取锁的时候,保持步调一致,即都先获取ID值小的账户,这样没有获得的就只能等获取的运行结束,释放锁后再去执行。经理代码变为:
public class CleverAccountManager
{
private readonly Account FromAccount;
private readonly Account ToAccount;
private double TransferAmount;
// private static readonly Mutex mutex = new Mutex();
public CleverAccountManager(Account accountFrom, Account accountTo, double accountTransfer)
{
FromAccount = accountFrom;
ToAccount = accountTo;
TransferAmount = accountTransfer;
}
public void Transfer()
{
object _lock1, _lock2;
(_lock1,_lock2)=FromAccount.ID<ToAccount.ID?(FromAccount,ToAccount):(ToAccount,FromAccount);
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {((Account)_lock1).ID}");
lock(_lock1)
{
Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {((Account)_lock1).ID}");
Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
Thread.Sleep(3000);
Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {((Account)_lock2).ID}");
lock (_lock2)
{
Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {((Account)_lock2).ID}");
FromAccount.WithdrawMoney(TransferAmount);
ToAccount.DepositMoney(TransferAmount);
}
}
}
}
运行结果如下:
Main Thread Started
Thread3 trying to acquire lock on 1001
Thread3 acquired lock on 1001
Thread3 Doing Some work
Thread4 trying to acquire lock on 1001
Thread3 trying to acquire lock on 1002
Thread3 acquired lock on 1002
Thread4 acquired lock on 1001
Thread4 Doing Some work
Thread4 trying to acquire lock on 1002
Thread4 acquired lock on 1002
Main Thread Completed
在实际项目中,死锁的问题不仅仅是一个技术问题,也是一个业务问题,因为从技术上来说,就是最开始那张图,说出花来,也就是怎样加锁解锁,当然你可以说不加锁不行吗?这也是可以的,现在不是有无锁数据结构吗?但是无论技术怎么变,总归是要为实际项目服务的。我们需要在学习和实践中不断总结!