深入理解死锁问题

news2024/11/26 8:46:29

死锁问题

    • 🏞️1. 死锁概念
    • 🌁2. 为什么发生死锁
    • 🌠3. 产生死锁的条件
    • 🌁4. 如何避免死锁
      • 📖4.1 循环等待
      • 📖4.2 持有并等待
      • 📖4.3 非抢占
      • 📖4.4 互斥
    • 🌿5. 通过调度避免死锁
    • 🍁6. 检查和恢复

🏞️1. 死锁概念

死锁(deadlock)是一种在许多复杂并发系统中出现的经典问题.

例如,当线程1持有锁L1,正在等待另外一个锁L2,而线程2持有锁L2,却在等待锁L1释放时,死锁就产生了:

Thread 1: Thread 2:
lock(L1); lock(L2);
lock(L2); lock(L1);

这段代码运行时,不是一定会出现死锁的,当线程1占有锁L1,上下文切换到线程2,线程2申请到锁L2,然后当它试图申请锁L1时,这时就产生了死锁,两个线程互相等待.

image-20221128113304755
我们来看一个简单的死锁案例:

#include<iostream>
#include<semaphore.h>
#include<pthread.h>
#include<unistd.h>

using namespace std;

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* threadRoutinue1(void* args)
{
    pthread_mutex_lock(&mutex1);
    sleep(2); //此时thread1睡眠, thread2会运行并拿到mutex2, thread1无法醒来时无法拿到mutex2
    pthread_mutex_lock(&mutex2);

    cout << "thread1 running" << endl;

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

void* threadRoutinue2(void* args)
{
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);

    cout << "thread2 running" << endl;
    
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
}

int main()
{
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, threadRoutinue1, nullptr);
    pthread_create(&t1, nullptr, threadRoutinue2, nullptr);

    cout << "main thread" << endl;

    pthread_join(t1, 0);
    pthread_join(t2, 0);

    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

🌁2. 为什么发生死锁

你可能在想,上文中提到的这个死锁的例子,很容易就可以避免,例如:只要线程1和线程2都用相同的抢锁顺序,死锁就不会发生,那么,死锁为什么还会发生?

其中一个原因是在大型的代码库中,组件之间会有复杂的依赖,以操作系统为例:虚拟内存系统需要访问文件系统才能从磁盘读到内存页;文件系统随后又要和虚拟内存交互,去申请一页内存,以便存放读到的块. 因此,在设计大型系统的锁机制时,你必须要仔细的去避免循环依赖导致的死锁.

另一个原因是封装. 软件开发者一直倾向于隐藏实现细节,以模块化的方式让软件开发更容易. 然而,模块化和锁不是很契合,某些看起来没有关系的接口可能会导致死锁.

🌠3. 产生死锁的条件

死锁的产生需要如下4个条件:

  • 互斥:线程对于需要的资源进行互斥的访问
  • 持有并等待:线程持有了资源(例如已经持有的锁),同时又在等待其他资源(例如,需要获得的锁)
  • 非抢占:线程获得的资源(例如锁),不能被抢占
  • 循环等待:线程之间存在一个环路,环路上的每个线程都额外持有一个资源,而这个资源又是另一个线程要申请的.

image-20221128113304755

🌁4. 如何避免死锁

📖4.1 循环等待

经常采用的预防技术,就是让代码不会产生循环等待最直接的方法就是获取锁时提供一个全序. 假如系统共有两个锁(L1L2),那么我们每次都先申请L1然后申请L2,就可以避免死锁. 这样的顺序避免了循环等待,也就不会产生死锁.

更复杂的系统中不会只有两个锁,锁的全序可能很难做到,因此,偏序可能是一种有用的方法,安排锁的获取并避免死锁.

全序和偏序都需要细致的锁策略的设计和实现,另外,顺序只是一种约定,粗心的程序员很容易忽略,导致死锁,而且有序加锁需要深入理解代码库,了解各种函数的调用关系.

image-20221128141721026

代码实例如下:

    //.......
    
    if(m1 > m2)
    {
        pthread_mutex_lock(&m1);
        pthread_mutex_lock(&m2);
    }
    else
    {
        pthread_mutex_lock(&m2);
        pthread_mutex_lock(&m1);
    }

    //.......

📖4.2 持有并等待

死锁的持有并等待条件,可以通过原子的抢锁来避免,例如通过如下代码:

lock(prevention);
lock(L1);
lock(L2);
...
unlock(prevention);

先抢到prevention这个锁后,代码保证了在抢锁的过程中,不会有不合时宜的线程切换,从而避免了死锁. 当然,这需要任何线程在任何时候抢占锁时,先抢到全局的prevention锁,这时,另一个线程就算使用不同的顺序抢锁,也不会有问题.

📖4.3 非抢占

在调用unlock之前,都认为锁是被占有的,多个抢锁操作通常会带来麻烦,因为我们等待一个锁时,持有另一个锁.

pthread_mutex_trylock()函数会尝试获得锁,没有获取到便返回-1,表示锁已经被占有,所以可以通过这一接口来实现无死锁的加锁方法:

top:
	lock(L1);
	if(trylock(L2) == -1)
    {
        unlock(L1);
        goto top;
    }

另一个线程可以采用相同的加锁方式,但是不同的加锁顺序,程序不会产生死锁.

但是又会带来一个新的问题:活锁. 两个线程有可能一直重复这一序列,又同时都抢锁失败,这种情况下,系统一直在运行这段代码,但是又不会有进展,因此名为活锁.

活锁的解决方法:可以在循环结束的时候,先随机等待一个时间,然后再重复整个动作,这样可以降低线程之间的重复互相干扰.

这个方案还有一个缺点如果代码在中途获取了某些资源,必须要确保也能释放这些资源,例如,在抢到L1后,我们的代码分配了一些内存,当抢L2失败时,并且在返回开头之前,必须确保能正确释放这些资源,无形中为我们增添了负担.

📖4.4 互斥

最后的预防方法是完全避免互斥,想法很简单:通过强大的硬件指令,构造出不需要锁的数据结构.

举个简单的例子:假设我们有比较并交换指令,是由硬件提供的一种原子指令,它会做如下的事情:

int CompareAndSwap(int* address, int expected, int new)
{
    if(*address == expected)
    {
        *address = new;
        return 1;  //交换成功
    }
    return 0; //交换失败
}

假如我们想原子的给某个值增加特定的数量,可以这样实现:

void AtomicIncrement(int* value, int amount)
{
    do
    {
        int old = *value;
    }while(CompareAndSwap(value, old, old + amount) == 0);
}

这种方式无须使用锁. 并且不会产生死锁(但这段代码有可能产生活锁).

🌿5. 通过调度避免死锁

有些场景更适合死锁避免,我们需要了解全局的信息,包括不同线程在运行中对锁的需求,从而使得后续的调度能够避免产生死锁.

例如,我们需要在两个处理器上调度4个线程,假设我们知道线程1(T1),需要锁L1L2T2也需要L1L2T3只需要L2T4不需要锁:

image-20221128150446315

即:只要T1T2不同时运行,就不会产生死锁:

image-20221128150818449

再来看一个竞争更多的例子:

image-20221128150909451

image-20221128151104547

线程T1、T2、T3执行过程中,都需要锁L1L2,所以需要让T1、T2、T3串行.

T1、T2、T3运行在同一个处理器上,这种保守的静态方案会明显增加完成任务的总时间,为了避免死锁,没有让它们并发运行,付出了性能的代价.

所以这种方法有两个缺点

  1. 需要提前知道所有任务以及它们需要的锁
  2. 会限制并发,降低性能.

🍁6. 检查和恢复

最后一种常用的策略就是允许死锁偶尔发生,检查到死锁时再采取行动,举个例子:如果一个操作系统一年死机一次,你会重启系统,然后愉快的继续工作.

很多数据库系统使用了死锁检测和恢复技术. 死锁检测器会定期运行,通过构建资源图来检查循环. 当循环(死锁)发生时,系统需要重启. 如果还需要更复杂的数据结构相关的修复,那么需要人工参与.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python开发】一文详解Flask-Login

一文详解Flask-LoginFlask-Login 为 Flask 提供用户会话管理。它处理登录、注销和长时间记住用户会话等常见任务。 Flask-Login 不绑定到任何特定的数据库系统或权限模型。唯一的要求是您的 用户对象实现一些方法&#xff0c;并且您向能够 从用户 ID 加载用户 的扩展提供回调。…

Kotlin 开发Android app(十二):Android布局FrameLayout和ViewPager2控件实现滚动广告栏

在上一节中我们简单的介绍了RecyclerView 的使用&#xff0c;他是整个开发的重点控件&#xff0c;这一节我们来看看FrameLayout 布局结合ViewPager2&#xff0c;开发一个广告控件。 新模块banner 先创建一个新的模块&#xff0c;取名为banner&#xff0c;用来创建我们的滚动广…

Spring Boot自定义Namespace

Spring Boot 自定义Namespace 在学些Spring Boot 自定义Namespace之前&#xff0c;先来看一个简单的案例。在Spring Boot出现之前&#xff0c;所有的bean都是在XML文件的格式 中定义。为了管理方便&#xff0c;一些大型复杂的应用系统&#xff0c;通常定个多个xml文件来共同满…

【笑小枫的按步照搬系列】JDK8下载安装配置

笑小枫&#x1f495; 欢迎来到笑小枫的世界&#xff0c;喜欢的朋友关注一下我呦&#xff0c;大伙的支持&#xff0c;就是我坚持写下去的动力。 微信公众号&#xff1a;笑小枫 笑小枫个人博客&#xff1a;https://www.xiaoxiaofeng.com 一、安装 1、方式一&#xff1a;进入官网…

Apifox:成熟的测试工具要学会自己写接口文档

好家伙&#xff0c; 在开发过程中&#xff0c;我们总是避免不了进行接口的测试&#xff0c; 而相比手动敲测试代码&#xff0c;使用测试工具进行测试更为便捷&#xff0c;高效 今天发现了一个非常好用的接口测试工具Apifox 相比于Postman&#xff0c;他还拥有一个非常nb的功…

读《基于深度学习的跨视角步态识别算法研究》

2020 背景&#xff1a; 作为一种新兴的识别技术&#xff0c;步态识别具有在非受控、远距离、低分辨率的场景下进行身份识别的优点&#xff0c;并且步态不易改变和伪装&#xff0c;所以近年来得到的关注逐渐增多。 步态识别作为一种新兴的身份识别技术&#xff0c;可以根据人…

jsp美食管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 美食管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统采用serlvet dao bean mvc模式开发&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式 开发。开发环境为TOMCAT7.0,Myeclipse8…

ZYNQ之FPGA学习----UART串口实验

1 UART串口简介 UART串口基础知识学习&#xff1a;硬件设计基础----通信协议UART 2 实验任务 上位机通过串口调试助手发送数据给 Zynq&#xff0c;Zynq PL 端通过 RS232 串口接收数据并将接收到的数据发送给上位机&#xff0c;完成串口数据环回&#xff0c;管脚分配如下&…

软件测试的分类

这里先讲一些概念&#xff0c;改日从这里边挑几个细讲。&#xff08;给小白看的&#xff09; 按测试对象划分&#xff1a; 界面测试&#xff1a; 软件只是一种工具&#xff0c;软件与人的信息交流是通过界面来进行的&#xff0c;界面是软件与用户交流的最直接的一层&#xff…

基于二次近似(BLEAQ)的双层优化进化算法_matlab程序

参考文献如上。 双层优化问题是一类具有挑战性的优化问题&#xff0c;包含两个层次的优化任务。在这些问题中&#xff0c;下层问题的最优解成为上层问题的可能可行候选。这样的要求使得优化问题难以解决&#xff0c;并使研究人员忙于设计能够有效处理该问题的方法。尽管付出了…

Redis常见面试问题总结

文章目录Redis 基础面试说说你对Redis的了解?说说Redis中的数据类型&#xff1f;说说Redis数据类型对应的数据结构&#xff1f;说说Redis对应的Java客户端有哪些&#xff1f;说说Redis 中持久化发生了什么&#xff1f;说说Redis中持久化以及方式&#xff1f;如何理解Redis中RD…

2022年超实用的推特营销策略

Twitter推广需知的13条基础知识&#xff1a; 1、Twitter日活用户达1亿 2、Twitter月活用户3.25亿 3、Twitter广告价格比其他渠道便宜33% 4、每天产生5亿条推文 5、Twitter推广能够提高29%的线下交易 6、37%的Twitter用户在18到29岁之间 7、86%的带链接推文会比普通推文效…

JUC并发编程与源码分析笔记03-CompletableFuture

Future接口理论知识复习 Future接口&#xff08;FutureTask实现类&#xff09;定义了操作异步任务执行的一些方法&#xff0c;如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。 找到java.util.concurrent.Future&#xff0c;看到里面…

Node.js 入门教程 22 将所有 Node.js 依赖包更新到最新版本

Node.js 入门教程 Node.js官方入门教程 Node.js中文网 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录Node.js 入门教程22 将所有 Node.js 依赖包更新到最新版本22 将所有 Node.js 依赖包更新到最新版本 当使用 npm install <packagename&g…

pytorch 手写数字识别1

目录 概述 加载图片 绘图部分 backward 前言&#xff1a; 这里以一个手写数字识别的例子,简单了解一下pytorch 实现神经网络的过程. 本章重点讲一下加载数据过程 参考&#xff1a; 课时9 手写数字识别初体验-1_哔哩哔哩_bilibili Pytorch中的backward函数 - …

为了让线上代码可追溯, 我开发了这个vite插件

人生的第一个vite插件 前言 想在控制台输出一下前端代码的一些构建信息&#xff0c; 比如打包时间、打包的人, 代码分支、commit是那个&#xff0c;方便在控制台追溯。 背景 遇到的问题 1、场景一 前端多人协同开发的情况下&#xff0c;比方测试站&#xff0c; 你发的代码…

Java 反射系列 —— 学习笔记

Java 反射系列 1. 类成员 为了更好的描述&#xff0c;我们做个约定个通配符 XXXX&#xff0c; 如果是成员变量就代表 Field&#xff0c;如果是类方法就代表 Method&#xff0c;如果是构造器就代表 Constructor。 1.1 获取方法 那么怎么获取到这三类成员呢&#xff1f; 获…

逆势涨薪3k!新媒体运营毅然转行测试,我的入行秘籍是什么?

不尝试永远都不会成功&#xff0c;勇敢的尝试是成功的一半。 大学毕业做运营&#xff0c;业务难精进&#xff0c;薪资难提升 “你大学专业是商务英语&#xff0c;为什么毕业后会选择做新媒体运营呢&#xff1f;” 其实我当时没有想那么多的&#xff0c;商务英语的就业方向一个…

苹果电容笔值得买吗?2022最新电容笔推荐

如今&#xff0c;许多人都喜欢用IPAD来学习记录&#xff0c;或是安静地作画。很多ipad的用户&#xff0c;都很重视它的实用性&#xff0c;因为他们发现&#xff0c;如果有一款功能不错的电容笔来搭配ipad&#xff0c;那么ipad的实用性就会得到极大的提高。事实上&#xff0c;如…

开发 Chrome 扩展程序的利弊

作为一名软件开发人员,您总是希望从事能够提高您的技术技能并赚钱的项目。有什么比开发现金流 chrome 扩展程序更好的方法呢? 在本文中,我将从软件开发人员的角度概述开发 chrome 扩展程序的一些优点和缺点。 开发 Chrome 扩展程序的好处 Chrome 扩展程序是软件开发人员接…