【Linux】面试重点:死锁和生产消费模型原理

news2024/11/27 12:52:30

面试要点来了~

文章目录

  • 前言
  • 一、死锁的一系列问题
  • 二、生产者消费者模型原理
  • 总结


前言

上一篇的互斥量原理中我们讲解了锁的原理,我们知道每次线程申请锁的时候一旦申请成功这个线程自己就把锁带在自己身上了,这就保证了锁的原子性(因为只有一个锁),而当我们已经申请成功锁了然后再去申请锁会发生什么事呢?下面我们在死锁中回答这个问题。


一、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
上面死锁的概念说一组进程中的各个进程,那么一个锁会引发死锁的问题吗?答案是会的,因为代码是程序员写的,所以一旦代码写的有问题即使是一把锁也会造成死锁的问题,比如说:申请锁却不去释放。申请了一把锁成功后又去申请同样的锁,这种情况下肯定会申请失败,一旦申请失败就导致该线程被阻塞挂起,那么这个锁就变成了死锁。申请了一把锁后又去申请其他的锁,结果其他的锁申请失败被阻塞,一旦被阻塞那么这个进程就会带着第一次申请成功的锁休眠,这样就造成死锁问题(因为第一把锁永远不会有线程申请成功了)。
当然造成死锁的条件有多种,下面我们直接给出结论:
1.互斥
2.请求与保持(我拿着我的然后申请你的)
3.环路等待(循环等待条件)
4.不剥夺条件
想要避免死锁,我们直接破坏上面4个条件中的任意一个即可。对于互斥这个条件想要破坏很简单,我们直接不加锁即可。对于请求与保持这个条件,我们只要主动的去释放锁那么就不会出现这样的问题。环路等待就是多个线程申请锁的顺序不一样,导致有的线程出现“请求与保持”的情况,对于这个问题我们只需要让每个线程按照顺序去申请锁即可。第四个条件的意思是:一个线程申请了锁然后只能自己释放锁,对于这样的情况一旦这个线程不去释放锁就会造成死锁问题,所以我们要剥夺这个线程的释放锁的条件,让其他线程去释放刚刚那个没有释放锁的线程。这里可能有些人会懵了,还可以一个线程申请锁,另一个线程释放刚刚那个线程申请的锁?答案是可以,下面我们用代码演示一下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void* args)
{
    cout<<"我是一个新线程"<<endl;
    pthread_mutex_lock(&mutex);
    cout<<"我拿到了锁"<<endl;
    pthread_mutex_lock(&mutex);//由于再次申请锁的问题会停下来(死锁)
    cout<<"我又活了过来"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadRoutine,nullptr);
    sleep(3);
    cout<<"主线程正在运行"<<endl;
    return 0;
}

首先我们先不让主线程释放锁,看一下死锁的状态:

 我们可以看到本来应该打印“我又活了过来”,结果直接退出了,下面我们让主线程给新线程解锁:

通过运行结果我们可以看到刚刚死锁的进程活过来了,那么就意味着可以一个线程申请锁,另一个线程释放锁。所以要破坏第四个条件直接控制一个线程统一释放锁。

下面我们总结一下:

避免死锁的方法:
1. 破坏死锁的四个必要条件
2. 加锁顺序一致
3. 避免锁未释放的场景
4. 资源一次性分配
下面讲解linux线程同步中的条件变量:
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
同步概念与竞态条件:
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。
下面讲一下条件变量函数初始化接口:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
参数:
cond :要初始化的条件变量
attr NULL
销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件变量:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond :要在这个条件变量上等待
mutex :互斥量
注意:条件变量带锁的意义是:当我们将某个线程放到条件变量中(可能队列等顺序容器)去等待的时候,条件变量为了防止死锁问题会自动将我们等待的线程的锁给释放掉,也就是说不然某个线程在等待的时候持有锁。
当然也有唤醒条件变量的接口:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
下面我们用代码演示这些接口如何使用:
const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *active(void* args)
{
    string name = static_cast<const char*>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex); //先加锁
        //然后放进条件变量去等
        pthread_cond_wait(&cond,&mutex); //调用的时候会自动释放锁
        cout<<name<<" 活动 "<<endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t tids[num];
    for (int i = 0;i<num;i++)
    {
        char* name = new char[32];
        snprintf(name,32,"thread -> %d ",i+1);
        pthread_create(tids+i,nullptr,active,name);
    }
    sleep(3);
    while (true)
    {
        cout<<"主线程唤醒其他线程......"<<endl;
        //在环境变量中按顺序唤醒一个线程
        pthread_cond_signal(&cond);
        sleep(1);
    }
    for (int i = 0;i<num;i++)
    {
        pthread_join(tids[i],nullptr);
    }
    return 0;
}

上面代码的作用是:首先创建5个线程,这5个线程都要进入active函数,在函数中首先加锁,然后让这个线程去环境变量中去等待,在等待的过程中会自己释放锁,然后3秒后主线程开始唤醒这些环境变量里的线程了,pthread_cond_signal这个接口的作用是每次唤醒一个线程,每次唤醒都是按顺序唤醒的,唤醒后这个线程就会打印“活动”,下面我们运行起来看看:

 可以看到这里唤醒的顺序都是31245,下面我们可以用一下pthread_cond_broadcast这个接口来一次唤醒所有的接口:

 通过运行结果我们可以看到同样是按顺序进行唤醒的,所以条件变量的作用是:允许多线程在cond中队列式等待(队列式等待就是一种顺序)。

下面我们总结一下:

为什么 pthread_cond_wait 需要互斥量 ?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex) ; 进入该函数后,会去看条件量等于0 不?等于,就把互斥量变成 1 ,直到 cond_ wait 返回,把条件量改成 1 ,把互斥量恢复
成原样。

 二、生产者消费者模型

下面我们先画张图理解一下生产者消费者模型:

 首先在模型中超市并不是属于生产者,超市只是一个交易场所,供货商会把生产的商品放到超市中,然后消费者直接去超市买。为什么消费者不直接去供货商那里购买呢?因为消费者不知道供货商什么时候生产好了商品,而且消费者的需求非常零散,供货商的生产水平很强,导致这两者之前有着天然的屏障。那么这个模型有什么特点呢?

第一点效率高,第二点忙闲不均。第二点是什么意思呢?意思就是说我们可以把供货商生产的一大批商品全部放在超市中让消费者随时随地来买,在供货商生产商品的时候是忙的,当生产的商品没有被购买或者还有很多商品存放的时候供货商不会生产太多,这个时候就是闲的。现在我们来把模型具体化,消费者实际上就是线程,生产者(供货商)也是线程,而超市这个交易场所实际上是一种特定的缓冲区(队列,链式,哈希等),而上面超市中的商品实际上就是数据。对于这个模型不知道大家能想到什么呢?没错就是管道通信,管道不就是一个进程把数据放在缓冲区中另一个进程去缓冲区中拿吗,我们当时写的一个关闭读端一个关闭写端的代码的例子不知道大家想起来没有。

对于我们刚刚说的缓冲区,这个缓冲区既要被消费者看到,也要被生产者看到,那么就注定了交易场所一定是一个会被多线程并发访问的公共区域,并且注定了多线程一定要保护共享资源的安全,注定了一定在这种情况下,要自己维护线程互斥与同步的关系。

下面我们讲解一下模型中的关系:

1.生产者和生产者:生产者和生产者之间一定是互斥的关系,因为他们都抢着要向缓冲区中存放资源,只有谁生产的快谁先放入缓冲区。

2.消费者和消费者:消费者和消费者之间的关系一定也是互斥的,因为当某个商品只有一个的时候那么两个消费者一定会去抢这个商品,这个时候就体现出互斥的关系了。

3.生产者和消费者:生产者和消费者之间是有同步关系的。因为当商品生产好后只有消费者消费了生产者才会继续生产,否则缓冲区是满的即使生产者再次生产也放不进去缓冲区。第二个生产者和消费者之间是有互斥关系的,因为我们不能让消费者一边在缓冲区拿东西一边让生产者往缓冲区送东西,生产者和消费者之间只能有一个进入缓冲区。

下面我们用一个简单的方法记录一下生产者消费者模型:

3种关系(生产者和生产者,消费者和消费者,生产者和消费者),两个角色(生产者和消费者),一个交易场所(通常是缓冲区)。所以我们以后记生产者消费者模型只需记住321即可。


总结

为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

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

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

相关文章

备忘录模式(二十二)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了中介者模式(二十一), 如果没有看过, 请观看上一章 一. 备忘录模式 引用 菜鸟教程里面备忘录模式介绍: https://www.runoob.com/design-pattern/memento-pattern.html 备忘录模式&#xff08;Memento Pattern&#xff…

基于Java单位人事管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

大面积无线WIFI覆盖 H3C WX3010E(AC+PoE三层交换机)+ H3C WA2620E、WA4320无线AP +华为USG6310S防火墙

一、适用场景&#xff1a; 1、跨复杂区域覆盖WIFI。支持多房间、多栋、多层复式楼、别墅、自建房的无线WIFI覆盖。 2、强大的漫游功能。楼上楼下移动使用WIFI时&#xff0c;需要支持WIFI的信号漫游&#xff0c;更换地理位置不掉线、不中断。 3、用户量或网络流量的负载均衡功…

IMX6ULL的官方SDK和官方BSP下载

买了块IMX6ULL的开发板&#xff0c;但是不想直接用开发板跟的程序&#xff0c;还有比如后面移植uboot和kernel的时候也想基于IMX6ULL官方的uboot和kernel做移植工作&#xff0c;所以自己先找一下怎么在官网下载这些东西。 1 官方SDK下载 百度搜索NXP官网&#xff0c;进去之后…

HotSpot虚拟机参数配置及优化

目录 一、JVM配置参数 二、GC回收日志分析 三、虚拟机性能监控和故障处理工具 1.命令工具 1)&#xff1a;基础工具 2)&#xff1a;性能监控和故障处理 2.可视化工具 四、JVM常出现问题 五、参考资料 一、JVM配置参数 HotSpot直到JDK9才提供统一的日志处理框架&#xff…

【k8s系列】使用MicroK8s 5分钟搭建k8s集群含踩坑经验

文章目录 MicrosK8s介绍版本选择准备三台虚拟机搭建MicroK8s环境安装Microk8s把当前用户加入Admin Group访问K8s启动和停止MicroK8s服务 搭建MicroK8s集群把Worker节点分别加入到MicroK8s集群在Master节点检查节点运行情况在Master节点打开存储插件在Master节点打开DNS插件查看…

低转速压榨,充分保留营养,用蓝宝原汁机每天轻享新鲜果汁

现在大家都特别重视健康&#xff0c;像是蔬菜、水果都是每天必需的&#xff0c;而且为了充分获取营养&#xff0c;很多人都会使用破壁机之类的工具&#xff0c;我觉得原汁机效果更好一些&#xff0c;它能够用慢速研磨技术来提取果汁&#xff0c;使用时不需要加水&#xff0c;能…

el-date-picker 结合dayjs 快速实现周选择、月选择,并设置控件的显示格式

目录 情况需求思路&#xff1a;使用el-date-picker 配置type属性&#xff0c;结合dayjsdayjs的安装以及常用api实现效果图 实现代码其他配置设置周选择控件显示一行为星期一 至 星期日 情况需求 在传递查询条件时&#xff0c;要求时间参数需要为 一周 或 一个月 思路&#xf…

系统移植 编译uboot和linux源码及驱动配置

写在前面&#xff1a;若是有些命令执行失败&#xff0c;前面添加sudo后再执行 目录 写在前面&#xff1a;若是有些命令执行失败&#xff0c;前面添加sudo后再执行 uboot源码获取和编译&#xff1a; Linux源码获取和编译 关于驱动配置 uboot源码获取和编译&#xff1a; 获…

驾驶安全、便捷,尽在车载Notification开发的掌握

Notification 概述 通知&#xff08;Notification&#xff09;是移动应用中常用的一种交互方式&#xff0c;用于向用户展示重要的信息、提醒事件或提供即时反馈等。通知可以以弹出窗口、图标或声音等形式呈现给用户。 以下是关于通知的一些基本概念和要点&#xff1a; 通知栏…

React基础教程(三):JSX语法

React基础教程(三)&#xff1a;JSX语法 1、JSX简介 全称&#xff1a;JavaScript XMLreact定义的一种类似于XML的JS扩展语法&#xff1a;JSXML本质是React.createElement(component, props, ...children)方法的语法糖作用&#xff1a;用来简化创建虚拟DOM&#xff08;注意&…

【数据库三】MySQL事务

MySQL事务 1.事务的概念2.事务的ACID特点3.知识点总结 1.事务的概念 事务是一种机制、一个操作序列&#xff0c;包含了一组数据库操作命令&#xff0c;并且把所有的命令作为一个整体&#xff0c;一起向系统提交或撤销操作请求&#xff0c;即这一组数据库命令要么都执行&#x…

JAVA0615_2

04JDK的下载和安装 05常用dos命令

golang-gin-mysql转gorm-struct--gen-model

背景:python-django项目用go-gin重构&#xff0c;数据库已存在&#xff0c;gin中使用gorm 所以需要工具将mysql数据库中的表结构转到orm的model中。 前提&#xff1a;因为国内访问github需要稳定的代理 Goproxy.cn 推荐这个 1.在项目路径中下载gen-model模块 go get -u git…

测试员怎么克服职业惯性获得成功?

长期从事质量控制的测试员&#xff0c;容易患上职业病——挑毛病、谈风险&#xff0c;踩刹车&#xff0c;大部分时候说的和做的都对。正所谓“手里拿个锤子&#xff0c;看什么都是钉子”。但事情成功者往往是那些有想法&#xff0c;有冲劲的乐观主义者&#xff0c;正所谓“悲观…

使用docker快速搭建redis哨兵模式

说明 本文主要参考&#xff1a; https://www.cnblogs.com/coderaniu/p/15352323.html https://developer.aliyun.com/article/892805 但是这两篇博客均缺失部分关键性细节&#xff0c;所以重新撰文。读者可以结合本文和上述文章一起阅读。 安装步骤 安装docker和docker-co…

【机器学习】——深度学习与神经网络

目录 引入 一、神经网络及其主要算法 1、前馈神经网络 2、感知器 3、三层前馈网络&#xff08;多层感知器MLP&#xff09; 4、反向传播算法 二、深度学习 1、自编码算法AutorEncoder 2、自组织编码深度网络 ①栈式AutorEncoder自动编码器 ②Sparse Coding稀疏编码 …

【Java基础学习打卡10】JDK下载与安装

目录 前言一、JDK下载1.JDK官网2.版本说明3.JDK 11下载 二、JDK安装1.JDK安装2.JDK安装目录介绍 总结 前言 本文主要介绍JDK 11 如何从官网下载&#xff0c;及如何在 Windows 11 系统安装&#xff0c;下载与安装很简单&#xff0c;主要是有少许的细节需要说明。 一、JDK下载 …

AI实战营第二期 第九节 《底层视觉与MMEditing》——笔记10

文章目录 AI实战营第二期 第九节 《底层视觉与MMEditing》什么是超分辨率图像分辨率的目标应用方向超分的类型单图超分的解决思路 深度学习时代的超分辨率算法SRCNNFast SRCNNSRResNet 感知损失 VS. 均方误差均方误差感知损失 对抗生成网络GAN应用于超分辨率如何学习生成器网络…

flutter:数据持久化

简单的数据持久化 保存数据到本地磁盘是应用程序常用功能之一&#xff0c;比如保存用户登录信息、用户配置信息等。而保存这些信息通常使用 shared_preferences&#xff0c;它保存数据的形式为 Key-Value&#xff08;键值对&#xff09;&#xff0c;支持 Android 和 iOS。shar…