【Linux】线程的互斥

news2024/11/25 15:00:40

目录

写在前面的话

相关背景概念

什么是互斥

互斥锁(互斥量)

互斥锁的使用

一些相关的问题

线程安全和可重入的区别


写在前面的话

        本文章主要介绍了线程的互斥的相关内容,而且本文的概念也比较多,所以需要有一些前提知识作为铺垫,可以观看我的前几篇关于线程的文章,最好对线程有个基本的理解和认知后,再阅读效果会更加好。

相关背景概念

        在了解Linux互斥时,我们需要先了解以下的概念:

  • 临界资源多线程执行流共享的资源就叫做临界资源(例如我们讲System V共享内存时,各进程通信的那一块区域便是临界资源)
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

什么是互斥

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

        为什么要这么做呢,因为多线程的话还会存在一个问题,看下面这个例子:


        多个线程 同时访问一个全局变量(所有线程共享),每个线程在执行时,都会进入一个循环,然后输出全局变量的值,每次将变量tickets--.

//如果多线程访问同一个全局变量,并进行数据计算,多线程会相互影响吗
int tickets = 10000;//在并发访问时,导致了数据不安全    
void* getTickets(void* args)
{
    while (true)
    {
        if(tickets > 0) //if判断本质也是一个计算的过程
        {
            usleep(1000);
            printf("%p: %d\n",pthread_self(),tickets);
            tickets--;
        }
        else 
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3;

    pthread_create(&t1,nullptr,getTickets,nullptr);
    pthread_create(&t2,nullptr,getTickets,nullptr);
    pthread_create(&t3,nullptr,getTickets,nullptr);

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);

}

我们运行这段程序,得到了这样的结果:

 

         出现了我们预料之外的结果-1,它是-1说明多减了一次,就像网上抢票,多售给了别人一张不存在的资源,这肯定就出问题了.

        造成这种问题的原因在哪里呢? 假设有a和b两个线程,此时tickets=1. a线程在进入if语句后,刚执行完第一条语句,还没有执行tickets--,就被切换 到b线程了,由于此时tickets还没有--,所有b也依然能进入if语句,b运气好直接执行完后续的代码了,所以此时tickets=0,然后切回到a继续执行后面的代码,此时tickets又--,便造成了tickets=-1的现象。

        所以多线程访问同一份共享资源是非常不安全的!


互斥锁(互斥量)

        为了解决这种问题,根源就是不要让多个线程同时访问同一份资源,所以我们只需要给临界资源"上个锁",然后当一个线程进去后,其它的线程便不能再进去执行,直至进去的这个线程执行完毕,别的进程才可以进入,这个锁便是互斥锁,也称为互斥量.

 

初始化锁

        这个锁的数据类型为pthread_mutex_t,创建出来需要进行初始化,

        这里需要用到一个函数,pthread_mutex_init来初始化锁,该函数原型如下:

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
        //如果定义的锁是全局的,则可以使用以下这种初始化
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • mutex:一个指向 pthread_mutex_t 类型变量的指针,用于存储初始化后的互斥锁。
  • attr:一个指向 pthread_mutexattr_t 类型变量的指针,用于指定互斥锁的属性。可以传入 NULL 使用默认属性

函数返回值:

  • 成功初始化互斥锁时,返回 0。
  • 若出现错误,返回一个非零的错误码

 后面会讲解使用。

加锁

        初始化完成之后,我们需要在临界区的前面加锁,加锁的函数为pthread_mutex_lock.

        该函数原型如下:

       int pthread_mutex_lock(pthread_mutex_t *mutex);

        其中参数为刚开始创建的锁,代表从这里开始加锁,多个线程访问时,只允许有一个线程进入,其他线程则阻塞.

函数返回值:

  • 成功加锁时,返回 0。
  • 若出现错误,返回一个非零的错误码

解锁

        加完锁后,需要在临界区后解锁,只有解完锁后,其他线程才可以进入,所以这里解锁的函数为pthread_mutex_unlock. 该函数原型如下:

       int pthread_mutex_unlock(pthread_mutex_t *mutex);

        同样地,参数是加锁的那把锁mutex

函数返回值:

  • 成功解锁时,返回 0。
  • 若出现错误,返回一个非零的错误码

互斥锁的使用

        学到了上面的函数,所以我们需要使用一下它们。整体流程是:我们首先要定义一把锁,然后初始化这把锁,紧接着在临界区入口处加锁,临界区出口处解锁.

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;;//pthread_mutex_t 就是原生线程库提供的一种数据类型

int tickets = 10000;//在并发访问时,导致了数据不安全    
void* getTickets(void* args)
{
    while (true)
    {
        //在临界区加锁
        pthread_mutex_lock(&mtx);
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s: %d\n",(char*)args,tickets);
            tickets--;
            //出了临界区就解锁
            pthread_mutex_unlock(&mtx);
        }
        else 
        {
            pthread_mutex_unlock(&mtx);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1,t2,t3;

    pthread_create(&t1,nullptr,getTickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,getTickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,getTickets,(void*)"thread 3");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);

}

这样变保证了这些线程不会同时访问这个临界资源,从而造成错误:

 但是由于加锁和解锁,造成了效率上一定的下降.

        当然可能由于sleep时间固定等各种因素,造成了只有一个线程在执行,我们可以加一个随机种子,使得sleep时间更加随机:

  这样各个线程便都参与进来了:


        回到最开始的初始化 ,当锁是全局变量的时候,可以直接使用PTHREAD_MUTEX_INITIALIZER进行初始化,但如果锁是局部变量的时候,那这个函数pthread_mutex_init该如何使用呢?

int main()
{
    pthread_mutex_t mtx;
    //初始化锁
    pthread_mutex_init(&mtx,nullptr);
    
    //...

    //释放和销毁锁
    pthread_mutex_destroy(&mtx);

    
    return 0;
}

那我们发现,既然锁是局部变量,那么每个函数要用的时候怎么办,都当做参数传进去吗?

        答案是必须得作为参数传入,但是这里可以有一些技巧:

要知道参数不仅仅可以是各种整型,数据类型,也可以是对象,所以我们可以在外面构造一个对象,然后每次创建线程时,将这个对象里面的内容同时也进行初始化,这样线程在调用回调函数时,直接把这个对象当做参数传入即可。

        

#define THREAD_NUM 5

struct ThreadData
{
public:
    ThreadData(const string& n,pthread_mutex_t* pm)
    :tname(n),
    pmtx(pm)
    {};


public:
    string tname;
    pthread_mutex_t* pmtx;
};

int main()
{
    //....
    pthread_t t[THREAD_NUM];
    for(int i = 0; i < THREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i);
        ThreadData* td =new ThreadData(name,&mtx);
        pthread_create(t+i,nullptr,getTickets,(void*)td);
    }
    //....
    return 0;
}

一些相关的问题

        Q1:加锁后 就是串执行了吗,加锁了以后,线程在临界区中会不会被系统调度切换走呢,会不会有什么问题呢,原子性体现在哪里呢?

       A:

         a.首先加锁后,这些线程以串行的方式执行临界区代码块。

        b.加锁了以后,线程在临界区 会被调度走,但不会有问题。因为虽然被调度切换了,但是是持有着锁被切换的,所以其它线程想要进入临界区,也必须现申请锁,但是由于此时锁还没有释放,所以其他线程会申请失败,也不会进来,保证了临界区数据的一致性!

        c.那原子性体现在哪里呢?在对一个没有持有锁的进程看来,最有意义的事情只有两个:

一 是某个线程没有持有锁(什么都没做);二 是某个线程释放锁(访问完临界资源了),此时我便可以访问临界资源了。这便是原子性的体现,对于某一个线程或进程,要么执行完毕,要么没有执行,不会有所谓的中间状态。


        Q2:要访问临界资源,每个线程都必须申请锁,要申请锁,必须先让不同的线程看到这同一把锁,所以锁本身也是一种共享资源,那么谁来保证锁的安全?

        A:为了保证锁的安全,申请和释放锁,内部操作一定是原子性的

        那么是如何保证锁内部是原子性的呢?即锁是如何实现的?

        整体上说,是靠一行汇编指令实现的,汇编指令它一定是原子性的。具体如下:

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下:

        以lock为例,假设就有a和b两个线程,al是一个CPU中的寄存器,a线程先进来,执行完第一条语句,将0写入到了al寄存器中,第二条语句还没有执行,b线程就来了,a必须带着自己的上下文al走了

        b也是执行完第一条语句,将0写入al寄存器,然后紧接着执行第二条语句,将al中的内容0和自己内存中的mutex的值进行交换(假设mutex值是1),此时mutex的值便成为了0,al值成为了1,

        紧接着a又过来把b换了,把自己的上下文填入到al寄存器中,al此时是0,然后与mutex交换,由于mutex与b线程的al=0之前进行了交换,所以此时mutex的值为0,所以和a线程中的al寄存器与mutex交换后依然是0,走到后面判读的时候,便把a挂起等待了,切到b执行,b由于此时al为1,可以正常执行,便成功申请到了锁.

线程安全和可重入的区别

        首先我们要清楚这两个的概念,线程安全是针对于线程来说的,而可重入与不可重入是对于函数来说的,是函数的一种特征.

  • 线程安全:当多个线程同时访问共享资源时,线程安全的代码能够保证在任何情况下都能正确地执行,并且最终保持数据的一致性。

常见线程安全情况:

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

常见的线程不安全情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 可重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为可重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见可重入情况:

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

常见不可重入函数:

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

线程安全与可重入区别

  •         可重入函数是线程安全函数的一种
  •         线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

到这里,线程互斥的内容就讲完了,下一章将介绍死锁和线程同步的相关概念及使用。

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

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

相关文章

【ES6】深入理解ES6(1)

一、块级作用域绑定 var声明及变量提升机制 二、字符串和正则表达式 字符串&#xff08;String&#xff09;是JavaScript6大原始数据类型。其他几个分别是Boolean、Null、Undefined、Number、Symbol&#xff08;es6新增&#xff09;。 更好的Unicode支持 1. UTF-16码位 字…

23款奔驰AMG GT50更换原厂运动排气系统,战斗感立马提升了

改装运动排气&#xff0c;原车中控的按键组也是需要更换的。与原车按键相比&#xff0c;只是多了一个排气的控制按键&#xff0c;也正是这个按键&#xff0c;能让车辆可静可怒&#xff0c;安静与怒吼就在一键之间。

QT-图标绘画工具

QT-图标绘画工具 一、效果演示二、关键程序三、程序链接 一、效果演示 二、关键程序 代码如下&#xff1a; #include "DrawDialogFactory.hpp" #include "DrawDialog.hpp" #include "GlobalDrawProperties.hpp"#include "Shape.hpp"…

pnpm常用命令

pnpm常用命令 下载pnpm&#xff0c;但是出现了 npm WARN notsup Unsupported engine for pnpm8.6.12: wanted: {"node":">16.14"} (current: {"node":"14.15.0","npm":"6.14.8"}) npm WARN notsup Not compa…

ViewUI表格Table嵌套From表单-动态校验数据合法性的解决方法

项目场景&#xff1a; 项目需求&#xff1a;在表格中实现动态加减数据&#xff0c;并且每行表格内的输入框&#xff0c;都要动态校验数据&#xff0c;校验不通过&#xff0c;不让提交数据&#xff0c;并且由于表格内部空间较小&#xff0c;我仅保留红边框提示&#xff0c;文字…

【编程指南】ES2016到ES2023新特性解析一网打尽

ES2016 Array.prototype.includes() Array.prototype.includes 方法&#xff1a; 这个方法用于检查数组是否包含特定元素&#xff0c;如果包含则返回 true&#xff0c;否则返回 false // 我有一个水果篮子 const fruitBasket [apple, banana, orange, grape];// 我要检查篮…

关于pycharm安装出现的interpreter field is empty,无法创建项目存储位置

关于pycharm安装出现的interpreter field is empty&#xff08;解释器为空&#xff09; 关于pycharm安装出现的interpreter field is empty&#xff0c;无法创建项目存储的位置。如图&#xff1a; 我之前安装的时候一直老是有这个提示&#xff0c;后来才发现是因为没安装这个p…

腾讯云服务器轻量和CVM有什么区别?

腾讯云轻量服务器和云服务器有什么区别&#xff1f;为什么轻量应用服务器价格便宜&#xff1f;是因为轻量服务器CPU内存性能比云服务器CVM性能差吗&#xff1f;轻量应用服务器适合中小企业或个人开发者搭建企业官网、博客论坛、微信小程序或开发测试环境&#xff0c;云服务器CV…

linux 安装go 1.18版本

首先去官网找到对应的版本 直接下载下来&#xff08;如果服务器可以直接访问到go 官网也可以wget直接下载到服务器&#xff09; 然后把该包上传到linux 的/usr/local 目录下 然后直接解压安装该包&#xff1a; sudo tar -C /usr/local -zxvf go1.18.10.linux-amd64.tar.gz 然…

通过Statement静态语句,实现CRUD操作

首先你需要创建 数据库 和 s_students学生表&#xff0c;再进行下一步的 增&#xff08;add&#xff09;&#xff0c;删(del)&#xff0c;改(update)&#xff0c;查(query)。 查询所有学生姓名&#xff1a; Testvoid query(){try{Statement st conn.createStatement();ResultS…

利用Torchmetrics库快速进行Torch的评价指标计算(推荐)

目录 1、安装 2、基本流程介绍 3、MetricCollection 4、自定义指标 5、我们可以调用多个指标计算不同的任务 6、可以是标签,也可以是one_hot编码 7、常用分类指标计算 8、异常报错 1、安装 官网地址:Welcome to TorchMetrics — PyTorch-Metrics 1.0.1 documenta…

JUL 日志 - 最简单易用的Java日志框架

在正式的生产环境下是不能使用 System.out 进行日志记录的 因为 System.out 不能提供时间、线程、执行过程 等信息&#xff0c;如果要手动打印输出则会非常麻烦 而日志就帮我们把这些事给干了 接下来我们学一个最简单的日志框架 JUL JUL全称Java util Logging是java原生的日志框…

用户数据报协议UDP

UDP的格式 载荷存放的是:应用层完整的UDP数据报 报头结构: 源端口号:发出的信息的来源端口目的端口号:信息要到达的目的端口UDP长度:2个字节(16位),即UDP总长度为:2^16bit 2^10bit * 2^6bit 1KB * 64 64KB.所以一个UDP的最大长度为64KBUDP校验和:网络的传输并非稳定传输,…

资源限制类题目解法,看这一篇就够了!

算法拾遗三十七资源限制类题目 资源限制技巧汇总32位无符号整数的范围是0~4,294,967,295&#xff0c;现在有一个正好包含40亿个无符号整数的文件&#xff0c;可以使用最多1GB的内存&#xff0c;怎么找到出现次数最多的数32位无符号整数的范围是0~4294967295&#xff0c;现在又一…

【VBA入门】WorkBook 对象 Name操作 宏录制筛选删除代码

VBA 入门 问题记录1 了解Excel工作簿、表格关系1 默认新建WorkBook2 新建WorkBook并命名工作表添加数据3新建带有指定数量工作表的工作簿 ActiveWorkbook.Names用法(1) 创建名称 (全局名称和局部名称&#xff09; 宏录制验证删除可行性大招&#xff01;&#xff01;&#xff01…

Linux基础与应用开发系列九:各类系统函数与标准IO函数

open_close函数 OPEN函数 头文件&#xff1a; #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> 函数原型&#xff1a; 当文件存在时 int open(const char* pathname,int flags) 当文件不存在时 int open (const char* pathname,int f…

(黑客)自学误区

一、自学网络安全学习的误区和陷阱 1.不要试图先成为一名程序员&#xff08;以编程为基础的学习&#xff09;再开始学习 行为&#xff1a;从编程开始掌握&#xff0c;前端后端、通信协议、什么都学。 缺点&#xff1a;花费时间太长、实际向安全过渡后可用到的关键知识并不多…

【2023最新美团笔试题目分析】“求最多出现数字及次数“、坦克大战在线对战游戏(问题描述 + 示例代码 + 时间复杂度分析)

弃幼少嬉戏堕慢之心,而衎衎于进德修业之志。 🎯作者主页: 追光者♂🔥 🌸个人简介: 💖[1] 计算机专业硕士研究生💖 🌿[2] 2023年城市之星领跑者TOP1(哈尔滨)🌿 🌟[3] 2022年度博客之星人工智能领域TOP4🌟 🏅[4] 阿里云社区特邀专家博主

光致发光二极管光源——荧光效率检测系统

发光二极管&#xff08;LED&#xff09;光源已经逐步地取代传统光源&#xff0c;并在生产和生活中得以广泛应用。荧光粉在LED照明设备中起到了至关重要的作用&#xff0c;其功能为将转换芯片所产生的紫外或者蓝光&#xff0c;发射出目标颜色的光。近年来&#xff0c;人们为了提…

计算文本相似度

目录 Python中的difflib模块模块用法报告涉及的符号实现文本对比普通文本对比文本对比生成HTML报告 余弦相似度sklearn安装使用sklearn的余弦相似度词袋模型 Jaccard相似度编辑距离&#xff08;Levenshtein距离&#xff09;TF-IDFWord2VecDoc2VecBERT结论 Python中的difflib模块…