当前位置: 首页 > ds >正文

Linux操作系统之线程(六):线程互斥

目录

前言

一、进程线程间的互斥相关背景概念

二、互斥量mutex

三、互斥量实现原理探究

四、封装mutex

总结:


前言

前文我们已经完成了对线程的简单封装,本文我们将开始对线程另外一个大阶段:线程的同步与互斥的学习。

本文将帮助大家了解线程互斥,锁的相关概念与知识。

注意,本文所用到的封装的thread,都是上一篇文章写好的代码。

一、进程线程间的互斥相关背景概念

要了解互斥,我们就需要先了解一下相关的背景概念。

临界资源:被多个线程(执行流)共享访问的资源(如全局变量、共享内存、文件、硬件设备等)。

临界区:每一个线程内部,访问临界资源的代码段,叫做临界区,注意是代码。

互斥:任何时刻,互斥都会保证有且仅有一个执行流进入临界区,访问临界资源。互斥通常对临界资源起保护作用,但是效率好不好就不一定了。通常会降低效率。

原子性:不会被任何调度机制(软中断等中断操作)打断的操作,该操作只有两种结果,要么完成,要么没完成。通常来说,转换成汇编时,只有一行代码的就是有原子性,否则是无原子性。


二、互斥量mutex

在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程的栈空间上,这种情况,变量归属于单个进程,其他线程理论上来讲不能获得这个变量。

但有些时候,有很多变量需要再线程间共享,这样的变量叫做共享变量,其卡退通过数据的共享实现线程之间的交互。一般来说,访问共享变量的代码默认情况下绝对不是原子的。

在有些时候,多个线程并发的操作共享变量,会给我们带来一些问题,大家可以看以下这段代码:

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include<string>
#include <unistd.h>int ticketnum = 1000;
void *ticket(void*argv)
{char* id=(char*)argv;while(true){if(ticketnum > 0){usleep(1000);//当做买票的那些加载,记录信息等操作std::cout << "ticketnum:" << ticketnum <<"线程id:"<<id<< std::endl;ticketnum--;}else{break;}}return nullptr;
}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,ticket,(void*)"tid1");pthread_create(&tid2,nullptr,ticket,(void*)"tid2");pthread_create(&tid3,nullptr,ticket,(void*)"tid3");pthread_create(&tid4,nullptr,ticket,(void*)"tid4");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);pthread_join(tid4,nullptr);return 0;
}

以上是一个非常简单的模拟我们黄牛抢票的操作,在这里我们使用了四个子线程并发式的调用,抢票。

按照代码逻辑,在我们if判断时就应该终止票数为0的操作了。

我们运行一下:

可是,为什么运行结果会出现0,-1,-2的打印呢?

难道我们的if条件判断语句没有生效吗?

 


我们先来解释一下造成这种结果的原因:

这是因为ticketnum--这个操作并不是原子的。

对于任何的++,--这类的操作代码,转换成汇编一办有三行指令:
mov  ticketnum eax

sub 减少值

mov eax ticketnum

也就是说,它不满足原子性。

这样会导致什么结果呢?

同学们,我们之前学过中断,也明白在操作系统中有一个时钟中断,定期的帮助操作系统调度进程,我们也知道每个进程都有一个时间片,时间片到了就会切换进程。

那么,我想问一下同学们,在这三个汇编指令的执行时期之间,会发生中断吗?

答案是:会中断!!!!

而我们的if条件判断也不是原子性的

所以,就会出现如下这种情况:

在我们线程1判断时,num>0成立,所以线程1进入了if语句中,但此时发生中断了,随后就该线程2执行了if条件判断,此时num还没减到0,所以线程2也满足进入if条件语句.......

所以我们会放进多个线程进入寄存器中去执行我们的--操作,而当这些线程恢复上下文时,接着执行我们未完成的代码,由于都已经进入了if判断,所以每个进入的线程最后都会让num--,所以就会出现打印数量为负数的情况。

我们总结一下:凭什么num会减到负数呢?

1、整个 "判断-操作" 过程(if + ticketnum--)不是原子的,导致多个线程可以同时进入临界区。

2、操作系统会让所有的线程尽可能多的进行调度切换执行。(线程通常会因为时间片耗尽,更高优先级的进程要执行,sleep返回用户态时进行时间片检测,而导致进程切换)


怎么解决上面的问题呢?

这里就要引出我们的互斥量:mutex的概念呢?

mutex锁会保护我们的资源,注意,保护资源,而不是保护每一个全局变量。

mutex会保护一段代码,这段代码通常不具备原子性操作。而mutex会让同一时间只有一个执行流进入我们临界区的代码中。防止出现上面的错误。

 

pthread库中自然也有相应的调用接口:

初始化:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

互斥锁销毁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

加锁与解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

这几个调用是较为常用的互斥量mutex的简单调用接口。

第一个是动态初始化,在运行时初始化:

  • mutex:指向要初始化的互斥锁。

  • attr:锁的属性(NULL 表示默认属性)。

第二个是静态初始化,编译时初始化,PTHREAD_MUTEX_INITIALIZER是一个宏。通常用这个是全局变量锁的初始化,无需手动销毁。

第三个用来销毁一个锁,释放互斥锁占用的资源。在销毁锁的时候需要注意:

使用PTHREAD_MUTEX_INITIALIZER初始化的锁不需要销毁。

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

已经销毁的互斥量,确保后面不会有线程再尝试加锁

第四个是加锁,第五个是解开锁。我们调用加锁函数的时候可以会遇见以下情况:
互斥量处于未锁状态,此时函数会将互斥量锁定,返回成功。

其他线程已经加锁,或存在其他线程同时申请互斥量,但没有竞争到,此时这个函数调用会陷入阻塞(执行流被挂起),等待互斥量解锁。这也就是我们加锁会影响效率的原因。

我们可以在上面ticket的代码中加上锁的代码来试一试:
 

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include<string>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int ticketnum = 1000;
void *ticket(void*argv)
{char* id=(char*)argv;while(true){pthread_mutex_lock(&mutex);if(ticketnum > 0){usleep(1000);std::cout << "ticketnum:" << ticketnum <<"线程id:"<<id<< std::endl;ticketnum--;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}return nullptr;}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,ticket,(void*)"tid1");pthread_create(&tid2,nullptr,ticket,(void*)"tid2");pthread_create(&tid3,nullptr,ticket,(void*)"tid3");pthread_create(&tid4,nullptr,ticket,(void*)"tid4");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);pthread_join(tid4,nullptr);return 0;
}

此时再运行代码,就不会出现之前为负数的情况了:


三、互斥量实现原理探究

1. 问题的本质:i++ 或 ++i 不是原子操作

在之前的例子中,我们已经发现,即使是简单的 i++ 或 ++i 操作,在多线程环境下也可能导致数据竞争(Data Race)。这是因为:

  • i++ 实际上包含多个步骤(读取→修改→写入),线程切换可能发生在任意步骤之间。

  • 如果多个线程同时执行 i++,可能会导致最终结果不符合预期(如 i 只增加 1 而非 2)。

2. 互斥锁的实现原理

为了保证操作的原子性,现代 CPU 提供了 原子交换指令(如 swap 或 exchange):

  • 原子交换指令的作用
    将 寄存器 和 内存单元 的数据进行交换,由于该操作是 单条 CPU 指令,因此具有原子性。

  • 多处理器环境下的保证
    即使多个 CPU 核心同时访问同一内存地址,总线仲裁机制也会确保 同一时刻只有一个 swap 指令能执行,其他 CPU 必须等待。

3. lock 和 unlock 的底层实现(伪代码)

我们全局资源,临界区资源被锁保护住了,但是锁也可能会是全局变量,那么谁来保护锁呢?

为了锁的安全性,所以锁被设计为硬件级别的原子性的操作,不会被线程调度打断。

基于 swap 指令,我们可以重新定义 lock 和 unlock 的底层逻辑(伪代码):

大家认为,哪一个汇编指令是加锁呢? 

没错,是xchgb %al ,mutex


四、封装mutex

为了方便我们后面代码的执行,我们可以把锁封装成一个对象,而不是去一个一个调用它的初始化,销毁接口,如图C++一样的mutex类:

锁的封装的代码简单,基本思路就是默认构造和默认析构函数中我们可以内部调用锁的初始化与销毁函数,随后加上我们的加锁与解锁的接口就行了。

值得一提的是我们可以使用采用RAII风格对锁进行管理,即定义一专门的类型,在初始化时把我们定义的Mutex类的变量传进去,在一些场景下通过创建局部变量,局部自动的生命周期销毁来进行锁的控制:

#ifndef _MUTEX_HPP_
#define _MUTEX_HPP_#include <pthread.h>namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}~Mutex(){pthread_mutex_destroy(&_mutex);}bool lock(){return pthread_mutex_lock(&_mutex) == 0;}bool unlock(){return pthread_mutex_unlock(&_mutex) == 0;}private:pthread_mutex_t _mutex;//互斥量锁};class LockGuard//采⽤RAII⻛格,进⾏锁管理{public:LockGuard(Mutex &mtx):_mtx(mtx)//通过后续使用时定义一个LockGuard类型的局部变量,在局部变量的声明周期内,互斥量会被自动加锁与解锁{_mtx.lock();}~LockGuard(){_mtx.unlock();}private:Mutex &_mtx;};
}#endif

如果采用我们自己的锁,那么上面的ticket代码就可以变成:
 

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include<string>
#include <unistd.h>
#include"mutex.hpp"MutexModule::Mutex mutex;int ticketnum = 1000;
void *ticket(void*argv)
{char* id=(char*)argv;while(true){//mutex.lock();MutexModule::LockGuard lockguard(mutex);if(ticketnum > 0){usleep(1000);std::cout << "ticketnum:" << ticketnum <<"线程id:"<<id<< std::endl;ticketnum--;//mutex.unlock();}else{//mutex.unlock();break;}}return nullptr;}
int main()
{pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,nullptr,ticket,(void*)"tid1");pthread_create(&tid2,nullptr,ticket,(void*)"tid2");pthread_create(&tid3,nullptr,ticket,(void*)"tid3");pthread_create(&tid4,nullptr,ticket,(void*)"tid4");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);pthread_join(tid4,nullptr);return 0;
}

我们这里可以通过LockGuard类(临时变量生命周期)来帮助我们进行管理,如果手动进行解锁上锁,难免会出现遗漏。


总结:

本文我们进行了互斥量的概念讲解,希望对大家有所帮助!!!

http://www.xdnf.cn/news/16174.html

相关文章:

  • SpringMVC快速入门之核心配置详解
  • 第十二章 用Java实现JVM之结束
  • 网络基础15-16:MSTP +VRRP综合实验
  • linux 环境服务发生文件句柄泄漏导致服务不可用
  • 基于网络爬虫的在线医疗咨询数据爬取与医疗服务分析系统,技术采用django+朴素贝叶斯算法+boostrap+echart可视化
  • CS231n-2017 Lecture5卷积神经网络笔记
  • 【世纪龙科技】电动汽车原理与构造-汽车专业数字课程资源
  • 33、基于JDK17的GC调优策略
  • haproxy七层均衡
  • CanOpen--SDO 数据帧分析
  • Hugging Face 模型的缓存和直接下载有什么区别?
  • 【C++】第十八节—一文万字详解 | map和set的使用
  • 7.22 Java基础 | I/O流【下】
  • 小米视觉算法面试30问全景精解
  • HCIA/IP(一二章)笔记
  • Redis 初识
  • vcs门级仿真(后仿真)指南
  • Linux研学-Tomcat安装
  • 深入解析Hadoop中的Region分裂与合并机制
  • [pdf epub]《软件方法》电子书202507更新下载
  • 如何安装没有install.exe的mysql数据库文件
  • C# 析构函数
  • 虚幻5入门教程:如何在虚幻5中创建一个C++的Plugin
  • Zabbix 6.0+ 使用官方模板监控 Redis 数据库的完整配置指南
  • Linux 内核不能直接访问物理地址,必须通过虚拟地址访问。
  • Java+Vue构建的固定资产内控管理系统,融合移动端便捷与后台管理强大功能,模块完备,提供全量源码,轻松实现资产智能管控
  • 【uboot/kernel1】启动流程,环境变量,内存,initramfs
  • 构建智能视频中枢--多路RTSP转RTMP推送模块在轨道交通与工业应用中的技术方案探究
  • 知识库搭建之Meilisearch‘s 搜索引擎 测评-东方仙盟测评师
  • 二分查找-852.山峰数组的峰顶索引-力扣(LeetCode)