Linux->线程互斥

news2024/12/23 10:32:47

目录

前言:

1 线程互斥

1.1 多线程并发问题

1.2 线程锁

1.3 锁的接口

2 线程安全与可重入函数

 3 死锁


前言:

        本篇文章主要讲解了线程互斥的实现方式,还有原理,并附上代码讲解。并且讲解了锁的概念,问题等。

1 线程互斥

        还记得我上一篇文章的结尾有提过的问题吗?如果多个线程同时访问同一个全局变量是否会导致什么问题呢?

        答案很明确,那就是一定会导致某种错误,那么这种错误是什么呢?请看下面的代码:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;

//1000张票
int Ticket = 1000;
#define NUM 5

//买票
void* buyTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        if(Ticket > 0)
        {
            usleep(1000);
            --Ticket;
            cout << name << "购买了一张票,还剩下:" << Ticket<< endl; 
        }
        else
        {
            //没票了就退出
            break;
        }

        usleep(10);
    }

    return nullptr;
}

//创建线程
int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; ++i)
    {
        char* name = new char[64];
        snprintf(name,64,"%s-%d","thread",i+1);
        int n = pthread_create(tids+i,nullptr,buyTicket,name);
        if(n != 0)
        {
            cout << "create thread fail" << endl;
        }
    }


    for(int i = 0; i < NUM; ++i)
    {
        int n = pthread_join(tids[i],nullptr);
        if(n != 0)
        {
            cout << "join thread fail" << endl;
        }
    }

    return 0;
}

        我先讲解一下这一段代码,首先,我们生成了5个线程,这5个线程都会去执行buyTicket这个函数,其中我们定义的Ticket是一个全局变量,所有的线程都能够访问到这个资源,并且它们都能对这个值进行修改,当这个值从1000变为了0的时候,所有的线程都会退出,那么最终的输出值一定是“某一个线程购买了一张票,还剩下0”,这个样式,但是请看我们的实际输出是什么?

        输出很奇怪欸,为什么会出现票的数量被减到了负数呢?这很明显不对吧,这确实不对,那么产生这个问题的原因是什么呢?这就涉及到了我们多线程并发访问的问题咯。

1.1 多线程并发问题

        我先问大家一个问题,那就是--Ticket这一句代码,在底层真的是一句指令吗?

        我的回答是并不是,现在我就为大家讲解一下,--Ticket的真正运行逻辑。

         请大家看到上图,我省略了虚拟地址空间这个步骤。在内存当中才会有真正的Ticket这个变量的位置,这个没问题,当我们进行--Ticket的时候,CPU会去将Ticket的内容加载进来,然后通过运算器运行了之后,再写回我们Ticket内存当中,那么也就是说,就算是我们简化的运算逻辑,都是分了三步才能对一个变量进行--操作,也就证明了我们的--Ticket这个过程并不是原子的。(原子表示一个动作只有完成或则没有开始两种动作,没有正在做这个过程)。

        记住了上面的结论,我们再来下一个过程,我们的OS会定期的切换PCB,没有任何一个执行流能够有特权,那么这就会导致一个什么问题呢?那就是当其中一个线程加载Ticket进入CPU,并且运算完成之后,正打算将数据写回到内存当中,这时,时间片到了,没办法只能就此作罢。然后另一个线程又来改Ticket这个变量,能改吗?能改,因为它是有权限的,所以他就疯狂的更改ticket的值,假设Ticket的值变为了100。然后时间片又到了,换回最开始的那个线程,他发现,我上次的Ticket还没有写回内存呢,所以赶紧写入了,Ticket就变为了999。这个时候下一个线程再来访问,Ticket的值正确吗?不对了吧。

        可能有小伙伴在这里会有一些疑问,那就是为什么CPU能够认识哪一个线程执行到了那个位置了呢?他又是如何辨别的?其实这一点可以归为一类,那就是我们的线程是由PCB的,还记得我上一篇文章当中讲了线程的私有属性当中有一个什么东西吗?一组寄存器量,没错,这组寄存器会记录下当前CPU里寄存器的值,也就是正在运行的代码的上下文,然后在下一次调用这个线程的时候,通过读取这些寄存器的变量,就能够知道上一次运行到了那个位置了。这就是线程切换不会出现问题的真正原因。

        所以因为有了这个问题,所以才出现了线程互斥这个概念。以及锁这个产物。

1.2 线程锁

        通过上面的讲解,我们已经明白了为什么会出现多线程访问并发的问题,所以咱们就需要想方法改进咯,那么我们的改进策略就是添加锁。

        那么什么是锁呢?其实就是字面意思,谁持有锁,谁就能够打开“某一扇门”,在我们的线程当中,这个门就是临界区的资源。什么又是临界区呢?所谓临界区其实就是含有临界资源的那部分代码,而临界资源又是什么?临界资源就是有可能会被多个线程同时访问的资源,上文的Ticket就是临界资源,所以我们上锁的位置就是线程访问临界区之前。

        我们的线程互斥也是基于锁来实现的,也就是说,当一份资源不允许被多个线程同时访问的时候,就需要对这一份资源添加一把锁,这样就可以让所有的执行流在运行到这个地方只能够单执行流运行,而不是多执行流并发访问。

        所以更改代码之后如下:

//锁的初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* buyTicket(void* args)
{
    string name = static_cast<const char*>(args);

    while(1)
    {
        //上锁
        pthread_mutex_lock(&mutex);
        if(Ticket > 0)
        {
            usleep(1000);
            --Ticket;
            cout << name << "购买了一张票,还剩下:" << Ticket<< endl; 
            //解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            //解锁
            pthread_mutex_unlock(&mutex);
            break;
        }

        //拿到票之后的其它动作
        usleep(10);
    }

    return nullptr;
}

        输出如下:

         我知道大伙现在脑袋里面懵懵的,首先第一个疑问,我们的临界区是因为不是原子的,从而导致了有多线程并发访问的问题,但是你的锁又是如何保证自己的原子性的呢?

        对于锁如何保证自己的原子性的其实很简单,并不是说它是由另一个锁保证的,而是说锁本身就是原子的。

        好,说到这里大伙估计更加懵比了,什么玩意哇?锁是原子的,那为什么之前的变量就不是了?这不是搞特殊对待嘛,最关键的是,这是怎么实现的。

        请看到锁实现的代码:

lock:(上锁)

        movb $0, %al

        xchgb %al, mutex

        if(al > 0){

                return 0;

        }else

                goto lock;

unlock:

        movb $1, mutex

        return 0;

        上面的代码是锁的实现,al是寄存器,mutex是锁,上锁的逻辑是先将寄存器的值置为0,然后再与mutex交换,如果交换之后,al从零变为了一个非零的值,那么就代表上锁成功,mutex的值也变为了0,程序正确返回,就能够继续向后执行,从而可以使用我们临界区的资源。如果锁之前就已经被拿走了,那么锁与寄存器交换之后,寄存器的值还是0,也就是表明了之前是有人上锁了,所以只能返回回去继续的判断,直到条件申请成功,上锁过程才会结束。

        而解锁过程就简单了,直接对锁置一,表示归还锁就行了。

        但是这样我并不能感受锁是原子性的,比如交换过程,我就感觉不是原子的,你的疑问确实是有道理的,但是事实上,这个过程就是原子的,因为这属于硬件层面的内容了,它使用了总线锁或者是缓存锁,以次来保证执行这一条指令时是不能够被打断的。

        第一个问题我解决了,第二个问题,为什么我在循环当中要添加一个usleep(10)这一句代码来模拟访问临界资源之后的动作呢?不加有什么影响嘛?

        删除usleep(10)的影响,输出:

         与之前的输出有什么不同?抢票的线程全部变为了1号线程了,这科学吗?这不科学,我明明是多个线程在访问的哇?

        其实这个问题理解起来很简单,那就是当前线程持有锁,当它释放锁了之后,后续有没有代码执行,他又返回了循环,又把锁给拿到了,其它线程又只能等待,因为它是正在执行的线程,所以他拿到锁的概率就是最大的。

1.3 锁的接口

        这部分博主就不细讲了,本身也没什么难度,只要记得锁的使用场景即可。

        锁的初始化,如果用函数接口,那么用了init之后就必须destory,如果用下面的这种方式就不需要这么做了:PTHREAD_MUTEX_INITIALIZER

         对于这个函数来说,mutex参数就是我们需要传入的锁,而attr表示锁的一些基本属性,我们不需要管,平时一般都是置为nullptr。

        锁的使用:

         lock表示上锁,unlock表示解锁,trylock是什么意思呢?其实很简单,trylock表示当我们多次的申请锁不行之后,他就不再申请锁了。

2 线程安全与可重入函数

概念:

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

        对于一个函数是否可重入其实并不是评判这个函数是否写的优秀与否,甚至可以说毫无关系,因为可重入函数与不可重入函数只是表示这个函数的性质而已。比如我们常见的printf函数他就是不可重入的函数,但是你能说它是写的不好的吗?不可能的。

        但是一个函数有线程安全问题,这确实是一个非常严重的事情,因为它会导致我们的程序在某种莫名其妙的情况下崩溃,根本无从下手更改Bug,所以线程安全问题必须规避。

常见的线程不安全的情况:

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

常见的线程安全的情况:

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性

 常见的不可重入的情况:

调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

 常见可重入的情况:

不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

 3 死锁

        相信大家学习了前面关于锁的相关知识,大家对于锁还是有一定的了解了,那么我提一个问题,假设现在有了两个线程,也有两份临界资源,假设资源为A,B,此时两个线程分别拿到了A和B,然后各自都想要去拿到另一份资源,请问它们谁会让步,让另一个线程先运行完呢?

        答案是,根本不可能,上面的情况就是死锁,因为上面的情况满了死锁的四个必要条件

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件 : 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件 : 若干执行流之间形成一种头尾相接的循环等待资源的关系

        因为死锁的产生条件必须是上面四个条件同时满足,那么破坏死锁的方式就是破坏其中一个条件即可。

        第一点,那就是不加锁,可能大伙觉得我有病,你让我加锁,为了规避线程安全,你又让我不加锁。大伙别急,其实我想说,对于一个程序而言,锁的使用是越少越好,如果一个程序时不时出现死锁问题,博主认为不如丢掉或则重构了,留着村村恶心自己。

        第二点,按照一定的顺序申请锁,也就是申请了A之后才能申请B,不能申请B之后再申请A,但是这种方式说实话只能避免一些常见的死锁情况,有些场景无法克服。

        第三点,当一个线程多次申请一把锁未果之后,那么它会释放自己持有了所有资源,避免导致其它线程申请自己的资源导致了死锁。        第四点,当已经出现了死锁问题,那么所有线程都别玩了,全都给我去重新排队玩,也就是所有线程释放所有持有的临界资源。


 

        以上就是博主对于线程互斥的所有理解了,希望能够帮助大家。还有一个问题留给大家,那就是我们的程序当中有一个Ticket变量,如果我们卖完了之后并不是break,而是我们会定期的放出一些票,但是买票线程并不知道,那么一直让他访问,上锁,解锁,合适吗?这也是关于我们线程同步的内容。

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

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

相关文章

C++进阶—继承(上)简单特性

目录 1.继承的概念及定义 1.1继承的概念 1.2 继承定义 1.2.1定义格式 1.2.2继承关系和访问限定符 1.2.3继承基类成员访问方式的变化 2.基类和派生类对象赋值转换 3.继承中的作用域 4.派生类的默认成员函数 5.继承与友元 6. 继承与静态成员 1.继承的概念及定义 1.1继承…

OpenSSL生成SSL证书,受浏览器信任吗?

OpenSSL是用于传输层安全(TLS)协议的开源工具包&#xff0c;OpenSSL生成SSL证书能受到浏览器信任吗&#xff1f;OpenSSL生成SSL证书能不能用于网站HTTPS加密呢&#xff1f; OpenSSL是什么&#xff1f; OpenSSL是基于密码学的用于传输层安全(TLS)协议的开源工具包&#xff0c;可…

【U8+】取消用友U8软件登录界面记住密码功能

【需求描述】 由于用友U8结合远程软件使用&#xff0c; 并且为了简化操作&#xff0c;远程用户建立一个公用账户&#xff0c; 所有的U8用户都使用同一个远程用户登录&#xff0c; 但是各自有U8的账号&#xff0c;登录账套的时候&#xff0c;有操作员记录密码后&#xff0c;别的…

Springboot 核心注解和基本配置解读

目录 1. Springboot 入门与原理 1.1 Springboot 简介 1.1.1 什么是Springboot 1.1.2 Springboot 主要优点 1.2 Springboot 相关注解 1.2.1 元注解 1.2.1.1 Target 1.2.1.2 Retention 1.2.2 Configuration 1.2.3 Import 1.2.3.1 直接注入 1.2.3.2 实现 ImportSelector…

Python基础语法2(超详细举例)

生活就是这样&#xff0c;有的时候即便你尽了最大努力&#xff0c;但依然无法得偿所愿 但是&#xff0c;难道向上攀爬的那条路不是比站在顶峰更令人热血澎湃吗&#xff1f; 文章目录 一、转义符 二、变量的赋值规则 三、数据类型 四、操作符 1.除法 2.幂运算 3.布尔运算…

读营销策划心得

读营销策划心得篇1 过去的一年可算是我工作上另一个转折点&#xff0c;更是一个新的开始。特别是自今年6月份接手营销策划工作&#xff0c;不知不觉&#xff0c;已有半年。回忆这一年的工作经历&#xff0c;有艰辛、有成长、有收获、更有前景。这一年既包含了太多的艰辛与不易&…

Redis【入门篇】---- 初始 Redis

Redis【入门篇】---- 初始 Redis 1. 认识NoSQL1. 结构化与非结构化2. 关联与非关联3. 查询方式4. 事务5. 总结 2. 认识Redis3. 安装Redis1. 依赖库2. 上传安装包并解压3. 启动4. 默认启动5. 指定配置启动6. 开机自启动 4. Redis桌面客户端1. Redis命令行客户端2. 图形化桌面客户…

2023年最新企业网盘排名!一文掌握各大企业网盘优缺点

近年来&#xff0c;企业网盘已经成为一个越来越流行的工具&#xff0c;为企业寻求简化他们的文件协作过程。由于团队成员分散在不同的位置和设备上&#xff0c;网盘提供了一种安全有效的方式来存储、共享和协作文件&#xff0c;为企业提供了一系列的好处&#xff0c;包括&#…

赚钱的底层模式和破局思路

赚钱的逻辑是什么&#xff0c;哪些价值观念的区别&#xff0c;让不同人在赚钱这件事情上产生巨大的差别&#xff1f; 如果从第一性原理出发&#xff0c;个体赚钱有哪些模式&#xff0c;以及如何优化&#xff1f; 一、出卖时间 本质上所有的赚钱方式都是出卖时间&#xff0c;…

车载开发中,蓝牙通信需要学习那些核心技术点?

车载蓝牙通信是指在汽车内部或车辆与外部设备之间使用蓝牙技术进行数据传输和通信。蓝牙5.0是现代蓝牙技术的最新版本&#xff0c;它引入了一系列新功能和改进&#xff0c;提供了更快的数据传输速度、更长的传输距离、更稳定的连接和更低的能耗。 那么车载蓝牙通信需要学习那些…

RabbitMQ实现延时消息的两种方法

RabbitMQ实现延时消息的两种方法 1、死信队列 1.1消息什么时候变为死信(dead-letter) 消息被否定接收&#xff0c;消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。消息在队列里得时间超过了该消息设置的过期时间&#xff08;TTL&#xff09;。消…

java 设计模式--创建者模式

参考&#xff1a;Java常见设计模式总结 概念 概念理解一&#xff1a;将复杂对象的创建过程分解在不同的方法中&#xff0c;不同的创建过程组装成不同对象。对象的创建与产品本身分离开&#xff0c;使得对象的创建过程更加清晰。例如&#xff1a;旅游套餐售卖场景。 一个套餐大…

@DateTimeFormat与@JsonFormat不完全解析

目录 前言测试代码DateTimeFormat不加任何注解的情况普通请求JSON请求 JsonFormat普通请求JSON请求 其他方式&#xff08;InitBinder&#xff09;结论源码地址 前言 一直以来对DateTimeFormat与JsonFormat 比较模糊&#xff0c;容易搞忘&#xff0c;今天就做个笔记&#xff0c…

【MySQL 利器之 mysqldump】

文章目录 前言一、mysqldump二、环境三、使用步骤1.服务器与服务器间直接同步2.导出到sql文件3.sql文件导入 总结使用方式 1 服务器间直连方式同步&#xff1a;使用中间SQL 文件方式&#xff1a;datax&#xff1a; 前言 1.随着服务器环境改造&#xff0c;新旧数据库环境更换&a…

微流控压力控制器和微流控注射泵的性能比较

摘要&#xff1a;针对微流控技术中的压力和流量控制&#xff0c;本文介绍了目前常用的两类装置&#xff1a;注射泵和压力泵&#xff0c;重点介绍了这两种装置的性能特点&#xff0c;并对这两种压力控制装置进行了简要的分析对比。分析结论是压力泵将逐渐替代注射泵的应用&#…

Addressable CRC设置详解

设置 Asset Bundle的CRC设置中有三个选项&#xff1a; Disable&#xff1b; Enable,InClude Cached; Enable,Excludeing Cached; 修改后实际改的是这里的选项&#xff1a; Disable 设置为Disable&#xff0c;实际上是将BundledAssetGroupSchema类的UseAssetBundleCrc参数设…

软考:软件工程:软件设计,总体设计,详细设计,耦合内聚流程图,NS图,PAD图,判定树判定图。

软考&#xff1a;软件工程: 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准备的 &#xff08;1&#…

【吃透网络安全】2023软考网络管理员考点网络安全(二)网络攻击详解

涉及知识点 黑客的攻击手段介绍&#xff0c;常见的网络攻击&#xff0c;软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 后面还有更多续篇希望大家能给个赞哈&#xff0c;这边提供个快捷入口&#xff01; 第一节网络管理员考…

多版本管理node.js

多版本管理node.js 1. 安装2. 配置使用2.1 修改node源2.2 常用命令 在Windows 计算机上管理node.js的多个安装版本。 这是朋友推荐的&#xff0c;就是自己在升级node的时候给搞崩了&#xff0c; 不得不提升效率&#xff0c;于是发现了这个好工具&#xff0c;可以反过来理解&…

金蝶云星空RCE漏洞复现

0x01 产品简介 金蝶云星空是一款云端企业资源管理&#xff08;ERP&#xff09;软件&#xff0c;为企业提供财务管理、供应链管理以及业务流程管理等一体化解决方案。金蝶云星空聚焦多组织&#xff0c;多利润中心的大中型企业&#xff0c;以 “开放、标准、社交”三大特性为数字…