并发编程 - 死锁的产生、排查与解决方案

打印 上一主题 下一主题

主题 831|帖子 831|积分 2493

在多线程编程中,死锁是一种非经常见的标题,稍不留意大概就会产生死锁,今天就和大家分享死锁产生的原因,如何排查,以及解决办法。
线程死锁通常是因为两个或两个以上线程在资源争取中,形成循环等候,导致它们都无法继承执行各自后续操纵的征象。
我们结合下图简朴举个例子,线程1拥有资源A同时使用锁A进行锁定,并等候获取资源B;与此同时线程2拥有资源B同时使用锁B进行锁定,并等候获取资源A。此时便形成了线程1和线程2相互等候对方先释放锁的征象,形成了死循环,终极导致死锁。

01、产生死锁的必要条件

根据死锁产生的原因,可以总结出以下四个死锁产生的必要条件。
1、互斥条件

互斥即非此即彼,一个资源要不是我拥有,要不是你拥有,就是不能我们俩同时拥有。也就是互斥条件是指至少有一个资源处于非共享状态,一次只能有一个线程可以访问该资源。
2、占据并等候条件

该条件是指一个线程在拥有至少一个资源的同时还在等候获取其他线程拥有的资源。
3、不可剥夺条件

该条件是指一个线程一旦获取了某个资源,则不可被强行剥夺对该资源的所有权,只能等候该线程自己主动释放。
4、循环等候条件

循环等候是指线程等候资源形成的循环链,比如线程A等候资源B,线程B等候资源C,线程C等候资源A,但是资源A被线程A拥有,资源B被线程B拥有,资源C被线程C拥有,云云形成了依靠死循环,都在等候其他线程释放资源。
02、代码示例

下面我们实现一个简朴的死锁代码示例,代码如下:
  1. //锁1
  2. private static readonly object lock1 = new();
  3. //锁2
  4. private static readonly object lock2 = new();
  5. //模拟两个线程死锁
  6. public static void ThreadDeadLock()
  7. {
  8.     //线程1
  9.     var thread1 = new Thread(Thread1);
  10.     //线程2
  11.     var thread2 = new Thread(Thread2);
  12.     //线程1 启动
  13.     thread1.Start();
  14.     //线程2 启动
  15.     thread2.Start();
  16.     //等待 线程1 执行完毕
  17.     thread1.Join();
  18.     //等待 线程2 执行完毕
  19.     thread2.Join();
  20. }
  21. //线程1
  22. public static void Thread1()
  23. {
  24.     //线程1 首先获取 锁1
  25.     lock (lock1)
  26.     {
  27.         Console.WriteLine("线程1: 已获取 锁1");
  28.         //模拟一些操作
  29.         Thread.Sleep(1000);
  30.         Console.WriteLine("线程1: 等待获取 锁2");
  31.         //线程1 等待 锁2
  32.         lock (lock2)
  33.         {
  34.             Console.WriteLine("线程1: 已获取 锁2");
  35.         }
  36.     }
  37. }
  38. //线程2
  39. public static void Thread2()
  40. {
  41.     //线程2 首先获取 锁2
  42.     lock (lock2)
  43.     {
  44.         Console.WriteLine("线程2: 已获取 锁2");
  45.         //模拟一些操作
  46.         Thread.Sleep(1000);
  47.         Console.WriteLine("线程2: 等待获取 锁1");
  48.         //线程2 等待 锁1
  49.         lock (lock1)
  50.         {
  51.             Console.WriteLine("线程2: 已获取 锁1");
  52.         }
  53.     }
  54. }
复制代码
在上面的代码中,thread1 先拥有lock1,然后实行获取lock2;thread2 先拥有锁住 lock2,然后实行获取lock1;由于线程间相互等候对方释放资源,以是导致死锁。
下面我们看看上面代码执行效果:

可以发现线程1和线程2都在等候彼此所拥有的锁。
03、排查死锁

上一节中我们编写了一个简朴的死锁代码示例,但是现实研发过程中代码不大概这么简朴直观,一眼就能看出来标题地点。因此如何排查发生死锁呢?
其实我们的开发工具Visual Studio就可以查看。可以通过调试菜单中窗口下的线程、调用堆栈、并行堆栈等调试窗口查看。
上面代码正常运行后,编辑器为如下状态,也没有报错,啥也看不出来。

在默认状态下是无法看出东西,此时我们只需要点击全部中断按钮,则死锁的相关信息都会展示出来,如下图。

可以看到已经提示检测到死锁了,同时在调用堆栈窗口中还可以通过双击切换具体发生死锁的代码。
我们再切换至并行堆栈调试窗口,和调用堆栈相比,并行堆栈窗口更偏向图形化,并且发生死锁的两个线程方法都有体现出来,同样可以通过双击切换到具体代码,如下图:

下面我们再来看看线程调试窗口,如下图,可以发现前面有两个箭头,此中黄色箭头表现当前选中的发生死锁的代码,图中绿色选中代码,灰色箭头表现第一个发生死锁的代码。可以通过双击当前窗口中行进行发生死锁代码的切换,如下图:

当然还可以通过其他方式排查死锁,比如分析dump文件,这里就不深入了,后面偶然机再单独讲解。
04、解决办法

下面介绍几种制止死锁的指导思想。
1、次序加锁

次序加锁就是为了制止产生循环等候,假如大家都是先锁定lock1,再锁定lock2,则就不会产生循环等候。
看看如下代码:
  1. //线程1
  2. public static void Thread1New()
  3. {
  4.     //线程1 首先获取 锁1
  5.     lock (lock1)
  6.     {
  7.         Console.WriteLine("线程1: 已获取 锁1");
  8.         //模拟一些操作
  9.         Thread.Sleep(1000);
  10.         Console.WriteLine("线程1: 等待获取 锁2");
  11.         //线程1 等待 锁2
  12.         lock (lock2)
  13.         {
  14.             Console.WriteLine("线程1: 已获取 锁2");
  15.         }
  16.     }
  17. }
  18. //线程2
  19. public static void Thread2New()
  20. {
  21.     //线程2 首先获取 锁2
  22.     lock (lock1)
  23.     {
  24.         Console.WriteLine("线程2: 已获取 锁2");
  25.         //模拟一些操作
  26.         Thread.Sleep(1000);
  27.         Console.WriteLine("线程2: 等待获取 锁1");
  28.         //线程2 等待 锁1
  29.         lock (lock2)
  30.         {
  31.             Console.WriteLine("线程2: 已获取 锁1");
  32.         }
  33.     }
  34. }
复制代码
我们看看代码执行效果。

2、使用实行锁

我们可以使用一些其他锁机制,比如使用Monitor.TryEnter方法实行获取锁,假如在指定时间内没有获取到锁,则释放当前所拥有的锁,以此来制止死锁。
3、使用超时机制

我们可以通过Thead结合CancellationToken实现超时机制,制止线程无限等候。当然可以直接使用Task,因为Task自己就支持CancellationToken,提供了内置的取消支持使用起来更方便。
4、制止嵌套使用锁

一个线程在拥有一个锁的同时只管制止再去申请另一个锁,这样可以制止循环等候。
上面是使用Thread实现的示例,现在大家直接使用Thread大概比力少,大多数都是使用Task,最后给大家一个Task死锁示例,代码如下:
  1. //锁1
  2. private static readonly object lock1 = new();
  3. //锁2
  4. private static readonly object lock2 = new();
  5. //模拟两个任务死锁
  6. public static async Task TaskDeadLock()
  7. {
  8.     //启动 任务1
  9.     var task1 = Task.Run(() => Task1());
  10.     //启动 任务2
  11.     var task2 = Task.Run(() => Task2());
  12.     //等待两个任务完成
  13.     await Task.WhenAll(task1, task2);
  14. }
  15. //任务1
  16. public static async Task Task1()
  17. {
  18.     //任务1 首先获取 锁1
  19.     lock (lock1)
  20.     {
  21.         Console.WriteLine("任务1: 已获取 锁1");
  22.         //模拟一些操作
  23.         Task.Delay(1000).Wait();
  24.         //任务1 等待 锁2
  25.         Console.WriteLine("任务1: 等待获取 锁2");
  26.         lock (lock2)
  27.         {
  28.             Console.WriteLine("任务1: 已获取 锁2");
  29.         }
  30.     }
  31. }
  32. //任务2
  33. public static async Task Task2()
  34. {
  35.     //线程2 首先获取 锁2
  36.     lock (lock2)
  37.     {
  38.         Console.WriteLine("任务2: 已获取 锁2");
  39.         //模拟一些操作
  40.         Task.Delay(100).Wait();
  41.         // 任务2 等待 锁1
  42.         Console.WriteLine("任务2: 等待获取 锁1");
  43.         lock (lock1)
  44.         {
  45.             Console.WriteLine("任务2: 获取 锁1");
  46.         }
  47.     }
  48. }
复制代码
:测试方法代码以及示例源码都已经上传至代码库,有爱好的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

徐锦洪

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表