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

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

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

我们结合下图简单举个例子,线程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()
{
    //线程1
    var thread1 = new Thread(Thread1);
    //线程2
    var thread2 = new Thread(Thread2);
    //线程1 启动
    thread1.Start();
    //线程2 启动
    thread2.Start();
    //等待 线程1 执行完毕
    thread1.Join();
    //等待 线程2 执行完毕
    thread2.Join();
}
//线程1
public static void Thread1()
{
    //线程1 首先获取 锁1
    lock (lock1)
    {
        Console.WriteLine("线程1: 已获取 锁1");
        //模拟一些操作
        Thread.Sleep(1000);
        Console.WriteLine("线程1: 等待获取 锁2");
        //线程1 等待 锁2
        lock (lock2)
        {
            Console.WriteLine("线程1: 已获取 锁2");
        }
    }
}
//线程2
public static void Thread2()
{
    //线程2 首先获取 锁2
    lock (lock2)
    {
        Console.WriteLine("线程2: 已获取 锁2");
        //模拟一些操作
        Thread.Sleep(1000);
        Console.WriteLine("线程2: 等待获取 锁1");
        //线程2 等待 锁1
        lock (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 首先获取 锁1
    lock (lock1)
    {
        Console.WriteLine("线程1: 已获取 锁1");
        //模拟一些操作
        Thread.Sleep(1000);
        Console.WriteLine("线程1: 等待获取 锁2");
        //线程1 等待 锁2
        lock (lock2)
        {
            Console.WriteLine("线程1: 已获取 锁2");
        }
    }
}
//线程2
public static void Thread2New()
{
    //线程2 首先获取 锁2
    lock (lock1)
    {
        Console.WriteLine("线程2: 已获取 锁2");
        //模拟一些操作
        Thread.Sleep(1000);
        Console.WriteLine("线程2: 等待获取 锁1");
        //线程2 等待 锁1
        lock (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()
{
    //启动 任务1
    var task1 = Task.Run(() => Task1());
    //启动 任务2
    var task2 = Task.Run(() => Task2());
    //等待两个任务完成
    await Task.WhenAll(task1, task2);
}
//任务1
public static async Task Task1()
{
    //任务1 首先获取 锁1
    lock (lock1)
    {
        Console.WriteLine("任务1: 已获取 锁1");
        //模拟一些操作
        Task.Delay(1000).Wait();
        //任务1 等待 锁2
        Console.WriteLine("任务1: 等待获取 锁2");
        lock (lock2)
        {
            Console.WriteLine("任务1: 已获取 锁2");
        }
    }
}
//任务2
public static async Task Task2()
{
    //线程2 首先获取 锁2
    lock (lock2)
    {
        Console.WriteLine("任务2: 已获取 锁2");
        //模拟一些操作
        Task.Delay(100).Wait();
        // 任务2 等待 锁1
        Console.WriteLine("任务2: 等待获取 锁1");
        lock (lock1)
        {
            Console.WriteLine("任务2: 获取 锁1");
        }
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云轴科技ZStack获评OpenCloudOS社区2024年度优秀贡献单位

近日,由 OpenCloudOS 社区主办的 2024 OpenCloudOS 年会在北京成功召开。本次大会以“稳建基石,共创新篇”为主题,汇集了业界顶级技术专家与行业领袖,共同探讨下一代操作系统的建设与未来。云轴科技ZStack作为OpenCloudOS 社区的重…

clickhouse解决suspiciously many的异常

1. 问题背景 clickhouse安装在虚拟机上,持续写入日志时,突然关机,然后重启,会出现clickhouse可以正常启动,但是查询sql语句,提示suspiciously many异常,如图所示 2. 问题修复 touch /data/cl…

从零开始k8s-部署篇(未完待续)

从零开始k8s 1.部署k8s-部署篇 1.部署k8s-部署篇 本次部署完全学习于华子的博客点击此处进入华子主页 K8S中文官网:https://kubernetes.io/zh-cn 笔者从零开始部署的k8s,部署前置条件为 1.需要harbor仓库,存放镜像,拉取镜像&am…

Dots 常用操作

游戏中有多个蚂蚁群落,每个蚂蚁属于一个群落,如何设计数据结构? 方法1:为蚂蚁组件添加一个属性 ID,会造成逻辑中大量分支语句,如果分支语句逻辑不平衡可能带来 Job 调度问题,每个蚂蚁会有一份蚂…

如何通过 Kafka 将数据导入 Elasticsearch

作者:来自 Elastic Andre Luiz 将 Apache Kafka 与 Elasticsearch 集成的分步指南,以便使用 Python、Docker Compose 和 Kafka Connect 实现高效的数据提取、索引和可视化。 在本文中,我们将展示如何将 Apache Kafka 与 Elasticsearch 集成以…

深入浅出:AWT的基本组件及其应用

目录 前言 1. AWT简介 2. AWT基本组件 2.1 Button:按钮 2.2 Label:标签 ​编辑 2.3 TextField:文本框 2.4 Checkbox:复选框 2.5 Choice:下拉菜单 2.6 List:列表 综合案例 注意 3. AWT事件处理 …

Go Energy 跨平台框架 v2.5.1 发布

Energy 框架 是Go语言基于CEF 和 LCL 开发的跨平台 GUI 框架, 具体丰富的系统原生 UI 控件集, 丰富的 CEF 功能 API,简化且不失功能的 CEF 功能 API 使用。 特性? 特性描述跨平台支持 Windows, macOS, Linux简单Go语言的简单特性,使用简单…

JS 异步 ( 一、异步概念、Web worker 基本使用 )

文章目录 异步代码异步执行概念ES6 之前的异步 Web worker 异步 代码异步执行概念 通常代码是自上而下同步执行的,既后面的代码必须等待前面的代码执行完才会执行,而异步执行则是将主线程中的某段代码交由子线程去执行,当交给子线程后&…

机器学习(二)-简单线性回归

文章目录 1. 简单线性回归理论2. python通过简单线性回归预测房价2.1 预测数据2.2导入标准库2.3 导入数据2.4 划分数据集2.5 导入线性回归模块2.6 对测试集进行预测2.7 计算均方误差 J2.8 计算参数 w0、w12.9 可视化训练集拟合结果2.10 可视化测试集拟合结果2.11 保存模型2.12 …

Java字符串操作利器:StringBuffer与StringBuilder类详解

在处理字符串变更时,StringBuffer和StringBuilder类是优选工具。与String类不同,StringBuffer和StringBuilder允许对象被多次修改,而不会生成新的未使用对象。 StringBuilder类自Java 5起引入,其与StringBuffer的主要区别在于Stri…

软件确认测试报告的内容和作用简析

软件确认测试报告是对软件确认测试过程及结果的正式记录,是评估软件质量的重要依据。它不仅对开发团队起到反馈作用,更是决策层判断软件是否可以交付的重要参考。 一、软件确认测试报告包括的内容   1、测试目的:明确此次测试的目的和所要…

结构体(初阶)

结构体: 结构体类型的声明 结构体初始化 结构成员访问 结构体传参 1.结构体的声明 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。 1.2结构的声明 struct tag { member - list; }variable-lis…

详解VHDL如何编写Testbench

1.概述 仿真测试平台文件(Testbench)是可以用来验证所设计的硬件模型正确性的 VHDL模型,它为所测试的元件提供了激励信号,可以以波形的方式显示仿真结果或把测试结果存储到文件中。这里所说的激励信号可以直接集成在测试平台文件中,也可以从…

React 第二十节 useRef 用途使用技巧注意事项详解

简述 useRef 用于操作不需要在视图上渲染的属性数据,用于访问真实的DOM节点,或者React组件的实例对象,允许直接操作DOM元素或者是组件; 写法 const inpRef useRef(params)参数: useRef(params),接收的 …

SQL子查询和having实例

有2个表如下;一个是站点信息,一个是站点不同时间的访问量, 现在要获取总访问量大于200的网站; 先执行如下sql,不包括having子句看一下,获得的是所有站点的总访问量; 这应是一个子查询&#xf…

【seatunnel】数据同步软件安装

【seatunnel】数据同步软件安装 下载 wget https://dlcdn.apache.org/seatunnel/2.3.8/apache-seatunnel-2.3.8-bin.tar.gz wget https://dlcdn.apache.org/seatunnel/seatunnel-web/1.0.2/apache-seatunnel-web-1.0.2-bin.tar.gz1、安装seatunnel Server 解压 tar zxvf ap…

散斑/横向剪切/迈克尔逊/干涉条纹仿真技术分析

摘要 本博文提供了多种数据类型的干涉条纹仿真,并展示了它们对应的散斑干涉条纹。还分别给出了横向剪切干涉以及剪切散斑干涉条纹的仿真。 一、迈克尔逊干涉与散斑干涉仿真 下图为干涉条纹与对应的散斑干涉条纹的仿真示意图。其中,干涉条纹可认为是源…

如何通过采购管理系统实现智能化采购?

随着人工智能、大数据等技术的快速发展,采购管理逐步迈入智能化时代。智能化采购不仅提升了效率,还为企业提供了更精准的采购决策支持。本文将从智能化采购的优势出发,探讨采购管理系统如何助力企业实现这一目标。 文中用到的采购管理系统&a…

【论文阅读笔记】IC-Light

SCALING IN-THE-WILD TRAINING FOR DIFFUSION-BASED ILLUMINATION HARMONIZATION AND EDITING BY IMPOSING CONSISTENT LIGHT TRANSPORT 通过施加一致的光线传输来扩展基于扩散模型的真实场景光照协调与编辑训练 前言摘要引言相关工作基于学习的基于扩散模型的外观和光照操纵光…

594: Maximum Tape Utilization Ratio

解法&#xff1a; 对于该题有以下错误&#xff08;敬希评论区指正 1.dp定义在全局会wa struct node {int count; // 当前容量下能够存储的程序数量int sum; // 当前容量下所占用的磁带长度vector<int> path; // 当前容量下选择的程序的路径&#xff08;存放的程序…