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

news/2025/3/1 3:28:11/文章来源:https://www.cnblogs.com/hugogoos/p/18634321

在多线程编程中,死锁是一种非常常见的问题,稍不留神可能就会产生死锁,今天就和大家分享死锁产生的原因,如何排查,以及解决办法。

线程死锁通常是因为两个或两个以上线程在资源争夺中,形成循环等待,导致它们都无法继续执行各自后续操作的现象。

我们结合下图简单举个例子,线程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
private static readonly object lock1 = new();
//锁2
private static readonly object lock2 = new();
//模拟两个线程死锁
public static void ThreadDeadLock()
{//线程1var thread1 = new Thread(Thread1);//线程2var thread2 = new Thread(Thread2);//线程1 启动thread1.Start();//线程2 启动thread2.Start();//等待 线程1 执行完毕thread1.Join();//等待 线程2 执行完毕thread2.Join();
}
//线程1
public static void Thread1()
{//线程1 首先获取 锁1lock (lock1){Console.WriteLine("线程1: 已获取 锁1");//模拟一些操作Thread.Sleep(1000);Console.WriteLine("线程1: 等待获取 锁2");//线程1 等待 锁2lock (lock2){Console.WriteLine("线程1: 已获取 锁2");}}
}
//线程2
public static void Thread2()
{//线程2 首先获取 锁2lock (lock2){Console.WriteLine("线程2: 已获取 锁2");//模拟一些操作Thread.Sleep(1000);Console.WriteLine("线程2: 等待获取 锁1");//线程2 等待 锁1lock (lock1){Console.WriteLine("线程2: 已获取 锁1");}}
}

在上面的代码中,thread1 先拥有lock1,然后尝试获取lock2;thread2 先拥有锁住 lock2,然后尝试获取lock1;由于线程间相互等待对方释放资源,所以导致死锁。

下面我们看看上面代码执行效果:

可以发现线程1和线程2都在等待彼此所拥有的锁。

03、排查死锁

上一节中我们编写了一个简单的死锁代码示例,但是实际研发过程中代码不可能这么简单直观,一眼就能看出来问题所在。因此如何排查发生死锁呢?

其实我们的开发工具Visual Studio就可以查看。可以通过调试菜单中窗口下的线程、调用堆栈、并行堆栈等调试窗口查看。

上面代码正常运行后,编辑器为如下状态,也没有报错,啥也看不出来。

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

可以看到已经提示检测到死锁了,同时在调用堆栈窗口中还可以通过双击切换具体发生死锁的代码。

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

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

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

04、解决办法

下面介绍几种避免死锁的指导思想。

1、顺序加锁

顺序加锁就是为了避免产生循环等待,如果大家都是先锁定lock1,再锁定lock2,则就不会产生循环等待。

看看如下代码:

//线程1
public static void Thread1New()
{//线程1 首先获取 锁1lock (lock1){Console.WriteLine("线程1: 已获取 锁1");//模拟一些操作Thread.Sleep(1000);Console.WriteLine("线程1: 等待获取 锁2");//线程1 等待 锁2lock (lock2){Console.WriteLine("线程1: 已获取 锁2");}}
}
//线程2
public static void Thread2New()
{//线程2 首先获取 锁2lock (lock1){Console.WriteLine("线程2: 已获取 锁2");//模拟一些操作Thread.Sleep(1000);Console.WriteLine("线程2: 等待获取 锁1");//线程2 等待 锁1lock (lock2){Console.WriteLine("线程2: 已获取 锁1");}}
}

我们看看代码执行结果。

2、使用尝试锁

我们可以使用一些其他锁机制,比如使用Monitor.TryEnter方法尝试获取锁,如果在指定时间内没有获取到锁,则释放当前所拥有的锁,以此来避免死锁。

3、使用超时机制

我们可以通过Thead结合CancellationToken实现超时机制,避免线程无限等待。当然可以直接使用Task,因为Task本身就支持CancellationToken,提供了内置的取消支持使用起来更方便。

4、避免嵌套使用锁

一个线程在拥有一个锁的同时尽量避免再去申请另一个锁,这样可以避免循环等待。

上面是使用Thread实现的示例,现在大家直接使用Thread可能比较少,大多数都是使用Task,最后给大家一个Task死锁示例,代码如下:

//锁1
private static readonly object lock1 = new();
//锁2
private static readonly object lock2 = new();
//模拟两个任务死锁
public static async Task TaskDeadLock()
{//启动 任务1var task1 = Task.Run(() => Task1());//启动 任务2var task2 = Task.Run(() => Task2());//等待两个任务完成await Task.WhenAll(task1, task2);
}
//任务1
public static async Task Task1()
{//任务1 首先获取 锁1lock (lock1){Console.WriteLine("任务1: 已获取 锁1");//模拟一些操作Task.Delay(1000).Wait();//任务1 等待 锁2Console.WriteLine("任务1: 等待获取 锁2");lock (lock2){Console.WriteLine("任务1: 已获取 锁2");}}
}
//任务2
public static async Task Task2()
{//线程2 首先获取 锁2lock (lock2){Console.WriteLine("任务2: 已获取 锁2");//模拟一些操作Task.Delay(100).Wait();// 任务2 等待 锁1Console.WriteLine("任务2: 等待获取 锁1");lock (lock1){Console.WriteLine("任务2: 获取 锁1");}}
}

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/859528.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一维数组、多维数组、Array(deepToString sort fill binarySearch)方法2024122620241226

数组20241226 [数组详情](深入理解 Java 数组 - 静默虚空 - 博客园)什么是数组: 数组是相同类型数据的有序集合注意:必须是相同数据数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成其中,每一个数据称作一个数组元素。 每个数组元素可以通过一个下标来访问它…

《计算机组成及汇编语言原理》阅读笔记:p116-p120

《计算机组成及汇编语言原理》学习第 7 天,p116-p120 总结,总计 5 页。 一、技术总结 1.CPU优化 (1)increase overall performance number 例如:16位电脑提升到32位电脑。 (2)multiprocessing One way to make computers more useful is to allow them to run more than on…

波折重重:Linux实时系统Xenomai宕机问题的深度定位过程

本文将带您深入了解一个与之相关的真实事故现场及其问题定位过程,波折重重,其中的xenomai问题定位思路具有一定借鉴意义,希望对你定位xenomai问题有所帮助。目录一 前言二 背景三 原因分析及措施硬件原因应用软件操作系统四 分析定位转机拨云见雾irq计数Schedstatcoreclk现象…

Java面向对象程序设计复习总结

作者:高世栋 学号:202302151071 一、第一章:初识Java与面向对象程序设计Java简介:Java是一种面向对象的程序设计语言,具有跨平台、安全性高、可移植性强等特点。面向对象程序设计概述:面向对象是一种程序设计思想,将现实世界的事物抽象为对象,通过对象之间的交互来完…

[Paper Reading] StegoType: Surface Typing from Egocentric Cameras

目录StegoType: Surface Typing from Egocentric CamerasTL;DRData数据采集设备开环数据收集闭环数据收集数据容错机制OracleMethodInput FeaturesBackboneDataLossExperiment效果可视化总结与思考相关链接Related works中值得深挖的工作资料查询 StegoType: Surface Typing fr…

JVM实战—2.JVM内存设置与对象分配流转

大纲 1.JVM内存划分的原理细节 2.对象在JVM内存中如何分配如何流转 3.部署线上系统时如何设置JVM内存大小 4.如何设置JVM堆内存大小 5.如何设置JVM栈内存与永久代大小 6.问题汇总1.JVM内存划分的原理细节 (1)背景引入 (2)大部分对象的存活周期都是极短的 (3)少数对象是长期存活…

【Obsidian】 博客园插件

搬运 原文作者:ZhangBlog 出处:https://www.cnblogs.com/aaalei/p/17926199.html由于 Markdown 语法的便捷性, 我们从繁重的排版布局工作中解脱出来, 越来越多的人开始接受这种写作方式, 该插件可以将你的 md 笔记, 方便的同步到博客园中, 即使你是使用的本地图片, 也无须担心…

【数据分析】如何构建数据分析体系?

一、数据分析体系的重要性二、如何搭建数据分析体系三、数据分析体系如何量体裁衣编者荐语: 很详细 以下文章来源于ruby的数据漫谈 ,作者ruby 摘要:在当今数字化时代,数据已成为企业决策和发展的重要依据。构建一个完善的数据分析体系,能够帮助企业从海量数据中挖掘价值,…

DDD你真的理解清楚了吗?怎么准确理解“值对象”

DDD你真的理解清楚了吗?我通过这一系列知识分享,让大家真正准确地理解DDD中这些晦涩的概念,今天探讨“值对象”这些年,随着软件业的不断发展,软件系统开始变得越来越复杂而难于维护。这时,越来越多的开发团队开始选择实践DDD领域驱动设计。领域驱动设计是一种非常优秀的软…

【AI+安全】sshd后门自动化检测 | BinaryAI在恶意软件检测场景的实践

原创 腾讯科恩实验室 腾讯科恩实验室 2024年11月12日 10:12 上海 一、引言 在网络安全攻防对抗中,攻击者经常通过在系统关键组件中植入后门程序,来获取持久的访问权限。sshd (SSH daemon) 作为管理远程登录的核心服务,是攻击者常用的目标之一。攻击者通过修改或者替sshd二进…

用Detr训练自定义数据

前面记录了Detr及其改进Deformable Detr。这一篇记录一下用Detr训练自己的数据集。先看下Detr附录中给出的大体源码,整体非常清晰。接下来记录大体实现过程 一、数据准备 借助labelme对数据进行标注然后将标注数据转换成COCO格式,得到以下几个文件其中JPEGImages存放所有图片…

8086汇编(16位汇编)学习笔记05.asm基础语法和串操作

https://bpsend.net/thread-121-1-2.htmlasm基础语法 1. 环境配置xp环境配置 1.拷贝masm615到指定目录 2.将masm615目录添加进环境变量 3.在cmd中输入ml,可以识别即配置成功dosbox环境配置 1.拷贝masm611到指定目录 2.将masm611所在目录添挂载进dosbox 3.将masm611目录在dosbo…