上一篇《C#中多线程的那点事-锁》,我们讲述了多线程编程过程中,一种限制多个线程对资源的同时访问的技术——锁。
小明同学,上周未和家人出去游玩去了。刚学了锁的用法,小明终于完善的模拟出了早餐店的流水线,所以他游玩很开心。但是回家的路上,却遇到了烦心事!
由于天气很好,小明一家人游玩到了天黑才驱车回家。正值交通拥堵的时候,在他们即将行进到一个环岛的时候,交通完全堵死了。
小明在车上看着道路资源被无限的占用着,联想到多线程编程中的锁:要是限制一下进入环岛的车辆的数量,是不是就不会出现这种无限的堵死在状态呢!
由于车辆太多,已经进入环岛的车辆,出环岛的路被堵死,无法出去,无法释放占用的道路资源。想要进入环岛的车辆,却又因为无法进入环岛而又一直占用着环岛的出口。这不就是个死循环嘛!
在堵了一个小时之后,小明一家人终于走过了这个环岛,不一会就到家了。
死锁演示
聪明的小明,直觉告诉他,在多线程中,也可能会出现这种现象。于是他用两个锁对象,模拟了环岛堵死的场景:
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(#34;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(#34;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;
}
}
}
}
系列文章
下面是给同学们准备的干货: