Linux操作系统学习(互斥)

news2024/11/13 19:15:16

文章目录

    • 线程安全
    • 互斥量
    • 互斥锁的原理
    • 线程安全补充
      • 可重入函数
      • 死锁

线程安全

​ 由于多个线程是共享同一个地址空间的,也就是很多资源都是共享的,那么线程通信就会很方便,但是方便的同时缺乏访问控制,可能会由于一个线程的操作问题,导致其他线程异常、崩溃、逻辑不正确等问题,这就是线程安全问题

​ 例如多个线程同时使用printf函数实际上在共享stdout资源,而stdout资源只有一个,多个线程都在使用它就可能导致打印出现乱码现象,只要涉及到全局的数据就会有线程安全问题。

下面设计一个抢票逻辑来验证一下线程安全问题

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

//抢票逻辑,1000张票,设5个线程同时抢
//tickets就是临界资源
//线程 在时间片到了、从内核态返回用户态时会进行切换
int tickets = 1000;

void* ThreadRun(void* args)
{
    int id = *(int*)args;
    delete (int*)args;
    while(true)
    {
        if(tickets > 0)
        {
            usleep(10000);
            std::cout << "线程[" << id <<"]正在抢票. . .剩余票数:" << tickets << std::endl;  
            tickets--;
        }
        else
            break;
    }
}

int main()
{
    pthread_t tid[5];
    for(size_t i = 0;i < 5;i++)
    {
        int* id = new int(i);
        pthread_create(tid+i,nullptr,ThreadRun,(void*)id);
    }

    for(size_t i = 0;i < 5;i++)
        pthread_join(tid[i],nullptr);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZmIa8Be-1677869555313)(G:\Typora\图片保存\image-20221224160153268.png)]

​ 上面的执行结果发现,最后抢票抢到了负数,很明显是出现了线程安全问题,实际中是绝对不能出现抢票抢到负数、两个人买到同一个票的问题

为什么会出现这种现象?

​ 当多个进程竞争CPU的时候,CPU为了保证每个进程能公平被调度运行,采取了处理任务时间分片的机制,轮流处理多个进程,每个进程都执行一段时间后切换至下一个进程不断循环直到执行结束,由于CPU处理速度非常快,在人类的感官上认为是并行处理,实际是伪并行,同一时间只有一个任务在运行处理。

​ 所以每个task_struct被cpu调度都是有时间片的,当线程1被cpu调度后时间片开始计时,同时cpu中的寄存器会产生线程1的上下文数据,当时间到了后,寄存器会记录线程1执行到哪,线程1会存储这些数据,下一次执行的时候再加载至寄存器中继续执行。

​ 假如票就剩1张了,线程1刚执行完票数减的代码时间片就到了就被切换至线程2;当线程1再一次到达运行队列顶端后加载他的数据到CPU中,寄存器会根据上下文数据,接着上一次的代码结束位置继续执行,那么此时线程1认为票数剩一张执行的自减,而实际是在线程1被切换下来的时候,ticktest又被线程2自减了1次,但是线程1再次被调度是接着上一次的代码结束的位置继续执行的,所以就出现了抢到负数的现象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WaU348c7-1677869555313)(G:\Typora\图片保存\image-20221224172316557.png)]

互斥量

上述抢票逻辑解决方案:对临界区的资源进行保护

方式:互斥

互斥:在任意时刻,只允许一个执行流访问临界区(即对临界区加互斥锁)

临界资源:像打印数据到显示器这样的就可以看作是临界资源,tickets是全局变量,就是一种临界资源

临界区:访问临界资源的代码区域

pthread库提供了互斥锁的相关函数,如下所示:

  • 初始化互斥量

    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    方式2:使用宏初始化 
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    
    • pthread_mutex_t:互斥量的数据类型,需要在全局(临界区)定义一个此类型的变量
    • restrict mutex:要初始化的的互斥量
    • restrict attr:相关属性设置,一般设置NULL交给OS去设置
    • 返回值:成功返回0,失败返回错误码
  • 销毁互斥量

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • 销毁互斥量

    • 注意:

      ​ 使用静态分配PTHREAD_MUTEX_INITIALIZER初始化的互斥量,不需要销毁

      ​ 不要销毁一个已经加锁了的互斥量

      ​ 已经销毁了的互斥量,要确保后面不会有线程再加锁。

    • 返回值:成功返回0,失败返回错误码

  • int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    • lock:加锁,线程调用该函数让互斥量上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用此函数的该线程将阻塞,直到该互斥锁变为可用为止
    • unlock:解锁,解除锁定 mutex 所指向的互斥锁
    • 返回值:成功返回0,失败返回错误码

上述代码变化如下:

class ticket
{
public:
    ticket()
        :tickets(1000)
    {
        pthread_mutex_init(&mtx,nullptr);//初始化锁
    }

    ~ticket()
    {
        pthread_mutex_destroy(&mtx);	//销毁锁
    }

    bool GetTicket()
    {
        bool ret = true;				//ret不是临界资源
        pthread_mutex_lock(&mtx);		//加锁
        if(tickets > 0)
        {
            usleep(1000);
            std::cout << "线程[" << pthread_self() << "]正在抢票. . .剩余票数:" << tickets << std::endl;  
            tickets--;
        }
        else
        {
            std::cout << "剩余票空" << std::endl;
            ret = false;
        }
        pthread_mutex_unlock(&mtx);		//解锁
        return ret;
    }
private:
    int tickets;
    pthread_mutex_t mtx;		//创建锁
};

void* ThreadRun(void* args)
{
    ticket* id = (ticket*)args;	//传入的是同一个对象,所以该对象是临界资源

    while(true)
    {
        if(!id->GetTicket())	//票余量空则退出
            break;
    }
}

int main()
{
    pthread_t tid[5];
    ticket* id = new ticket();
	//创建线程
    for(size_t i = 0;i < 5;i++)
        pthread_create(tid+i,nullptr,ThreadRun,(void*)id);
	//线程等待
    for(size_t i = 0;i < 5;i++)
        pthread_join(tid[i],nullptr);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIBK658a-1677869555314)(G:\Typora\图片保存\image-20221225012034475.png)]

​ 上述代码在堆区申请了一个ticket类,又把首地址做为参数传给函数,所有线程访问的ticket就是同一个了(临界资源),在 pthread_mutex_lock(&mtx);[.....] pthread_mutex_unlock(&mtx);之间的执行流就是互斥的,串行执行。

但是要注意: GetTicket函数中的bool变量不是临界资源,它是在栈区的,谁使用谁创建,函数执行完后销毁

互斥锁的原理

要访问tickets,就要先访问mtx,mtx需要被所有线程看到,那么锁也是一种临界资源,如何保证锁的安全呢?

下面用一段加锁/解锁伪代码来解释互斥锁的原理:

lock:   movb 	$0  %al				//线程A、B在CPU上运行时,情况al寄存器
        xchgb 	%al mutex			//线程A把mutex与寄存器内容交换,线程A运行时CPU的al寄存器就有个对应数据
        if(al寄存器内容 > 0)			 //线程B运行时虽然也进行交换,但是mutex为0,交换完的al还是0
        {									
            return 0;//加锁成功		 //线程A继续往下执行时检查它在CPU运行时的al寄存器判定有锁
        }							//线程B继续往下执行时,检查它在CPU上运行时,al寄存器没对应数据,即无锁,将该线程
        else						//挂起等待(PCB被挂到等待队列)
        {							//即便CPU时间片到了中途切换走:A运行时的,会把它在寄存器中相关数据存储 抱着锁走的
            //挂起等待				 //B也同理,所以在下一次CPU调度A或B运行时,只需要检测al寄存器的数据
        }							//而其他线程、后来创建的线程,无论如何也拿不到锁:
        goto lock					//1.mutex是全局的,已经由A在拿锁是交换为0了,在后续判断就会被判定无锁,随后被挂起
									//2.A在cpu上运行的时间片到了,也会抱着锁走的(存储自己运行时寄存器产生的数据)
unlock:
		movb 	$1 mutex			//只能等A解锁,重置mutex的值,并唤醒等待的线程(处于S状态的PCB)
        唤醒等待线程
        return 0
  • al是寄存器,被多个线程共享,但是数据不是共享的

    ​ CPU有一组寄存器,其中就有al,al可以说是共享的,但是al的数据是不一样的;每个线程被CPU调度后,al寄存器会产生相应的数据,当线程的时间片到了以后数据会被线程存储,下个线程被调度后,又会把它的数据加载到寄存器中。所以每个线程在被调度时都会有al寄存器,但是他们的数据是私有的。

  • 伪代码中的xchgb可以看成是swap或者exchange

    ​ 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的 数据相交换,由于只有一条指令,保证原子性

    • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

加锁:

  • 线程执行加锁函数时,执行到movb $0 %al,把al寄存器清0

  • 之后会把内存当中的变量mutex 与 寄存器al进行数据交换(mutex一开始是1)

  • 之后进行对寄存器进行判断,判断是否为0,不为0则表示加锁成功,为0则表示锁被占用,把当前竞争锁的线程挂起等待

  • 中途若是时间片到了,是会把al寄存器的内容一起存储的,也就是”抱着锁被走的“,下一次被调度又会把数据重新加载到寄存器

解锁:

  • 当拿到锁的线程执行解锁函数时,先把mutex重新置1,其他的线程会被唤醒等待之后会经goto跳转到第一个行重新执行

注意:

  1. 带锁执行是比不带锁执行要慢一些的,因为加锁解锁也需要时间

  2. 只要有一个线程拿到锁,其他线程就会被挂起等待,除非拿到锁的线程解锁唤,否则其他线程是无法访问临界区的,从而保证了线程安全

  3. 所以为什么加锁函数可以保证原子性,主要是因为,核心争锁的部分是这一条交换语句 xchgb %al mutex,只会出现执行了这条语句和没执行这条语句的情况;

    • 看谁先执行这条语句,谁就拿到了锁,就算执行完后时间片到了或者被中断了那也是已经争到了锁。

    而自己设置个全局变量,利用++ 、-- 赋值等操作模仿加锁是不行的,这些操作没有原子性。

    • 例如++,在汇编层面需要三条语句才能全部执行完++的逻辑功能;若一个线程执行时中途发生中断,下一次接着中断位置执行完,而其他线程在中断期间执行过++了,可能全局变量的数据就异常了。

线程安全补充

​ 多个线程并发同一段代码时,不会出现不同的结果。常见于全局变量或者静态变量进行操作,并且没有锁保护的情况下,会导致线程不安全。例如多个线程打印hello 可能会出现乱序

线程安全的情况:

  1. 每个线程对于全局会在静态变量只有读权限,没有写权限

  2. 类或者接口对于线程来说时原子的

  3. 多线程切换不会导致结果出现二义性。

    反之就是不安全的

可重入函数

可重入函数:同一函数被不同执行流调用,当前线程还没执行完,就有其它进程进入,我们称之为重入。一个函数在重入的情况下,运行结果不会有任何问题,则该函数称为可重入函数,否则就是不可重入函数。

不可重入函数如:malloc、new、free、io操作的相关函数等

可重入函数与线程安全的问题:

  1. 函数是可重入的,那就是线程安全的
  2. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  3. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

死锁

​ 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。

例如,拿了锁却没释放,之后又去申请锁,导致这个有锁的线程也挂起等待,从而变成永久等待

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-meZYrcEk-1677869555314)(G:\Typora\图片保存\image-20230207184534035.png)]

死锁的必要条件:

  • 不可剥夺(不能改):执行流获取了互斥锁之后,除了自己主动释放锁,其他执行流不能解该互斥锁

  • 循环等待:线程A等待线程B拿的锁,线程B等待线程A拿的锁

  • 互斥条件(不能改):一个互斥锁,只能被一个执行流在同一时刻拥有

  • 请求与保持:线程A拿着 1 锁还想请求 2 锁,线程B拿着 2 锁还想请求 1 锁

只有同时满足上述4点,才会导致死锁;破坏死锁的其中一个必要条件即可避免

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

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

相关文章

元数据管理、治理、系统、建设方案、范例等

【数据治理工具】–元数据系统 1.元数据系统 1.1 概述 如果想建设好元数据系统&#xff0c;需要理解元数据系统的相关概念&#xff0c;如数据、数据模型、元数据、元模型、ETL、数据血缘等等。 首先&#xff0c;要清楚数据的定义、数据模型的定义。数据一般是对客观事物描述…

全国程序员薪酬大曝光!看完我酸了····

2023年&#xff0c;随着互联网产业的蓬勃发展&#xff0c;程序员作为一个自带“高薪多金”标签的热门群体&#xff0c;被越来越多的人所关注。在过去充满未知的一年中&#xff0c;他们的职场现状发生了一定的改变。那么&#xff0c;程序员岗位的整体薪资水平、婚恋现状、职业方…

Halo开源建站工具

目录 特性 代码开源 易于部署 插件机制 附件管理 搜索引擎 快速开始 最新主题 下载安装主题 开发者指南 我的本地站点 docker管理 本地站点 gaghttps://halo.run/ 支持h2文件系统存储数据&#xff0c;支持docker部署。 特性 我们会一直探索&#xff0c;追求更好…

【JavaSE】方法的使用初学者易懂

前言 大家好&#xff0c;我是程序猿爱打拳。今天讲解的是Java中方法的使用。Java中的方法类似于C语言里面的函数其中都有实参与形参。但Java中的方法又比C语言中的函数更为强大&#xff0c;为何呢&#xff1f;请看下文。 目录 1.为什么要有方法&#xff1f; 2.方法的概念及使…

Centos 虚拟机安装

文章目录Centos 虚拟机安装一、模版虚拟机环境准备安装VMvare&#xff0c;安装CentosCentos 虚拟机安装 一、模版虚拟机环境准备 安装VMvare&#xff0c;安装Centos 创建虚拟机&#xff0c;然后选择自定义安装 然后是默认的&#xff0c;点一下步 这一步选择稍后安装操作系…

Java下浅谈String.valueOf()

今日遇到遇见无语的事情&#xff0c;mybatis查询数据库结果 List<Map<String, String>> 需要转换为字符串&#xff0c;但是在debug时&#xff0c;在idea小窗口单独执行代码&#xff0c;是可以正常编译的&#xff0c;离开idea小窗口执行就报错&#xff1a; 类型转换…

Anaconda安装Pytorch(win系统)

前面有一篇博客专门讲了安装CPU版本的Pytorch&#xff0c;因为当时没有GPU&#xff0c;现在有了3090&#xff0c;专门记录一下安装GPU版的过程。一、添加清华源可参考官方anaconda | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror创建虚拟环境若没有…

一文解决Vue所有报错【持续更】

前言 Vue是一个流行的前端框架&#xff0c;许多web开发人员使用Vue来构建他们的应用程序。然而&#xff0c;正如任何其他框架一样&#xff0c;Vue也可能会发生错误。在这篇技术文章中&#xff0c;我们将探讨Vue常见的报错以及如何解决它们。 常见错误 1. Vue Template Error …

【目标检测】61、Dynamic Head Unifying Object Detection Heads with Attentions

文章目录一、背景二、方法2.1 scale-aware attention2.2 spatial-aware attention2.3 task-aware attention2.4 总体过程2.5 和现有的检测器适配2.6 和其他注意力机制的关联三、效果四、代码论文链接&#xff1a; https://arxiv.org/pdf/2106.08322.pdf代码链接&#xff1a;htt…

一文带你了解阿里的开源Java诊断工具 :Arthas

Arthas 是阿里开源的 Java 诊断工具&#xff0c;相比 JDK 内置的诊断工具&#xff0c;要更人性化&#xff0c;并且功能强大&#xff0c;可以实现许多问题的一键定位&#xff0c;是我用到的最方便的诊断工具。 下载和安装见官网 https://arthas.aliyun.com/doc/profiler.html 下…

Gem5模拟器,如何在linux系统中查看内存、CPU、硬盘、进程、网络等信息(十二)

虽然说&#xff0c;这个记录的是与Linux相关的操作&#xff0c;每次查每次忘&#xff0c;必须写一个来归总一下&#xff0c;以免我漫山遍野找命令。但是不想新开一一个主题&#xff0c;再加上确实是在运行模拟器时会关注这方面的信息&#xff0c;就把这一节搁这儿啦。 常见的查…

MedCalc v20.217 医学ROC曲线统计分析参考软件

MedCalc是一款医学 ROC 曲线统计软件,用于ROC曲线分析的参考软件,医学工作者设计的医学计算器,功能齐全。它可以帮助医生快速作出普通的医学计算,从而对症下药。提供超过76种常用的规则和方法,包括:病人数据、单位参数、费用计算等等。甚至可以将图形另存为BMP,PNG,GIF…

ATL中__if_exists的替代方案

__if_exists 和 __if_not_exists 是什么? __if_exists 和 __if_not_exists 是微软 ATL (Active Template Library&#xff0c;活动模板库) 中的关键字&#xff0c;可以用来在编译期间测试一个标识符是否存在。如果该标识符存在&#xff0c;则其关联的语句将会被执行。 __if_e…

2023年3月软考中级(系统集成项目管理工程师)报名走起!!!

系统集成项目管理工程师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职…

【Vue】Vue常见的6种指令

Vue的6种指令-前言指令&#xff08;Directives&#xff09;是vue 为开发者提供的模板语法&#xff0c;用于辅助开发者渲染页面的基本结构。vue 中的指令按照不同的用途可以分为如下6 大类① 内容渲染指令 ② 属性绑定指令 ③ 事件绑定指令 ④ 双向绑定指令 ⑤ 条件渲染指令 ⑥ …

Fortinet设备审计

作为网络安全领域的领导者&#xff0c;Fortinet提供了多种网络安全解决方案&#xff0c;包括下一代防火墙&#xff0c;即FortiGate。通过EventLog Analyzer的FortiGate预定义报表以及其他Fortinet应用程序的详尽列表&#xff0c;充分发挥Fortinet设备的最大作用。FortiGate您的…

粒子群算法

粒子群算法1 粒子群算法介绍2 基本思想3 算法流程4 代码实现1 粒子群算法介绍 粒子群优化算法(PSO&#xff1a;Particle swarm optimization) 是一种进化计算技术&#xff08;evolutionary computation&#xff09;。源于对鸟群捕食的行为研究。粒子群优化算法的基本思想是通过…

我建议,专家不要再建议了

作者| Mr.K 编辑| Emma来源| 技术领导力(ID&#xff1a;jishulingdaoli)关于买房&#xff0c;专家建议&#xff1a;不建议掏空六个钱包凑首付。&#xff08;网友&#xff1a;丈母娘等不到我自己挣够&#xff09;关于农村剩男多&#xff0c;城市剩女多&#xff0c;专家建议&am…

Adobe illustrator学习笔记

Adobe illustrator学习笔记 生命有限&#xff0c;设计无限 学习Adobe illustrator主要是用来制作SVG图片去制作字体图标&#xff0c;因此笔记内容大多会围绕SVG展开。 2023-3-5 1、Adobe illustrator简介 主要用于制作矢量图和插图 2、颜色模式介绍 显示颜色&#xff1a;RGB…

EXCEL里的各种奇怪计算问题:数字后面自动多了 0.0001, 数字后面位数变成000,以及一些取整,数学函数

1 公式计算后的数&#xff0c;用只粘贴数值后&#xff0c;后面自动多了 0.0001&#xff0c;导致不再是整数的问题 问题入戏 见第1个8400&#xff0c;计算时就出现了问题&#xff0c;按正常&#xff0c;这里8400应该是整数&#xff0c;而不应该带小数&#xff0c;但是确实就计…