过多的锁定也会有麻烦。在死锁中,至少有两个线程被挂起,并等待对方解除锁定。由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去。
为了说明死锁,下面实例化 StateObject 类型的两个对象,并把它们传递给SampleTask 类的构造函数。创建两个任务,其中一个任务运行 Deadlock1() 方法,另一个任务运行 Deadlock2() 方法:
var statel = new StateObject();
var state2 = new StateObject();
new Task(new SampleTask(statel, state2).Deadlock1).Start();
new Task(new SampleTask(statel, state2).Deadlock2).Start();
Deadlock1() 和 Deadlock2() 方法现在改变两个对象 s1和 s2 的状态,所以生成了两个锁。Deadlock1() 方法先锁定 sl,接着锁定 s2。Deadlock2() 方法先锁定 s2,再锁定 sl。现在,有可能Deadlock1() 方法中 sl 的锁定会被解除。接着,出现一次线程切换,Deadlock2() 方法开始运行,并锁定 s2。第二个线程现在等待sl 锁定的解除。因为它需要等待,所以线程调度器再次调度第一个线程,但第一个线程在等待 s2 锁定的解除。这两个线程现在都在等待,只要锁定块没有结束,就不会解除锁定。这是一个典型的死锁。
public class SampleTask
{
public SampleTask(StateObject sl, StateObject s2)
{
_sl = sl;
_s2 = s2;
}
private StateObject _sl;
private StateObject _s2;
public void Deadlock1()
{
int i = 0;
while (true)
{
lock (_s1)
{
lock (_s2)
{
_s1.ChangeState(i);
_s2.ChangeState(i++);
Console.WriteLine($"still running, {i}");
}
}
}
}
public void Deadlock2()
{
int i = 0;
while (ture)
{
lock (_s2)
{
lock (_s1)
{
_s1.ChangeState(i);
_s2.ChangeState(i++);
Console.WriteLine($"still running, {i}");
}
}
}
}
}
结果是,程序运行了许多次循环,不久就没有响应了。“仍在运行” 的消息仅写入控制台中几次。同样,死锁问题的发生频率也取决于系统配置,每次运行的结果都不同。
死锁问题并不总是像这样那么明显。一个线程锁定了 s1,接着锁定 s2;另一个线程锁定了 s2,接着锁定 s1。在本例中只需要改变锁定顺序,这两个线程就会以相同的顺序进行锁定。但是,在较大的应用程序中,锁定可能隐藏在方法的深处。为了避免这个问题,可以在应用程序的体系架构中,从一开始就设计好锁定顺序,也可以为锁定定义超时时间。
微信公众号
Dotnet讲堂