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

【Linux系统】线程的同步与互斥(1)——互斥量mutex

文章目录

  • 引入问题
  • 一、线程互斥
    • 1.1、相关概念
    • 1.2、简单介绍互斥量
    • 1.3、互斥量的相关接口
    • 1.4、互斥量的原理
    • 1.5、互斥量的封装

引入问题

通过对线程的相关概念的学习,我们知道一个进程内部有多个线程,而所有的线程都共享进程地址空间,因此,进程的大部分资源都会被线程共享。那么对于共享的资源,如果多个线程同时访问它会产生什么后果呢🤔??

我们在学习线程控制的相关操作时,我们时常会见到这样一个现象:多个线程向显示器打印数据时,会出现严重的信息干扰。
在Linux眼中,显示器本质上也就是一个文件,也是一个共享资源,多线程访问共享资源,必然会引发数据不一致问题
如何解决呢🤔??这就需要我们学习本文所讲的同步与互斥相关内容了。


一、线程互斥

1.1、相关概念

🔥临界资源🔥
多线程执行流被保护的共享资源就叫做临界资源。
🔥临界区🔥
每个线程内部,访问临界资源的代码,就叫做临界区。
🔥互斥🔥
任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
🔥原子性🔥
不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。


1.2、简单介绍互斥量

大部分情况,线程使用的数据都是局部变量,变量处于对应线程的栈空间内,这种情况,变量归属单个线程,其他线程是无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
当多个线程并发的操作共享变量时,会引发一些问题。我们来看一下代码💻:

// 操作共享变量会有问题的售票系统代码#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<pthread.h>inttickets=100;void*route(void*arg){char*id=(char*)arg;while(true){if(tickets>0){usleep(1000);// 模拟抢票花费时间printf("%s sells ticket:%d\n",id,ticket);tickets--;}else{break;}}returnnullptr;}intmain(){pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,route,(void*)"thread 1");pthread_create(&t2,nullptr,route,(void*)"thread 2");pthread_create(&t3,nullptr,route,(void*)"thread 3");pthread_create(&t4,nullptr,route,(void*)"thread 4");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return0;}

结果如下:

这个结果显然不符合我们的期望,并且还有点违反常识🤯。

问题1️⃣:为什么会数据不一致呢🤔??

我们在计算机组成原理这门课中学过:CPU可以处理两种运算,一个是算术运算,另一个是逻辑运算。而我们代码中对tickes大小的判断就属于逻辑运算,在运算之前,CPU会将内存中对应的tickets值导入到寄存器中,而这一过程正是导致问题的元凶之一。

除了代码中的if语句可能会造成数据不一致问题,代码中的--操作同样也不可忽视,--操作本身也不是原子的。在C/C++中,tickets--是一条语句,但在CPU眼中,实际为三条汇编语句:1️⃣将内存中的tickets移入寄存器exa中;2️⃣对exa减1;3️⃣再将exa中的tickets传入内存。因此,很有可能对exa减之前或将tickets数值更新前就进行线程切换,而导致数据不一致问题(数据重复)。对全局变量++或–,非常容易引发线程安全问题。

问题2️⃣:我们如何解决这一问题呢🤔??

tickets属于共享资源,为了避免共享资源一次性被多次访问,我们应该将共享资源保护起来,而被保护的共享资源,我们也称之为临界资源。因此一句话概括就是:将共享资源转化为临界资源

保护共享资源不被多线程同时访问,需要满足以下三点:
1️⃣代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2️⃣如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3️⃣如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而要满足以上三点,我们仅需要使用Linux提供的互斥量mutex(也称互斥锁)即可。

在我们的代码中,if判断语句中多次访问了临界资源,因此,根据临界区的定义可知,临界区的范围如下:

因此,保护临界资源的本质就是保护临界区,保护临界区的手段就是利用互斥,也就是利用


1.3、互斥量的相关接口

上文,我们简单引出了互斥量的相关话题,接下来,我们将简单介绍互斥量的相关接口。
相关接口都包含在<pthread.h>

1️⃣初始化互斥量
初始化互斥量有两种方法:一种是静态分配,也就是利用宏来初始化;

pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER

另一种则是动态分配。

intpthread_mutex_init(pthread_mutex_t*restrict mutex,constpthread_mutexattr_t*restrict attr);

其中mutex参数就是我们要初始化的互斥量attr是对应互斥量的状态,一般我们不必理会,将它设为NULL即可。

2️⃣销毁互斥量

intpthread_mutex_destroy(pthread_mutex_t*mutex)

其中mutex参数就是我们要销毁的互斥量
在销毁互斥量的时候,我们要注意以下三点:

  1. 使用静态分配初始化的互斥量不需要销毁。
  2. 不要销毁一个已经加锁的互斥量。
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

3️⃣加锁与解锁

intpthread_mutex_lock(pthread_mutex_t*mutex);// 加锁intpthread_mutex_unlock(pthread_mutex_t*mutex);// 解锁

当加锁或解锁成功返回0,失败返回对应的错误号。

简单介绍相关接口后,接下来我们对先前的抢票系统进行优化。

可以发现,通过加锁等操作,的确避免了数据不一致问题。但是从运行结果来看,为什么抢到票的都是同一个线程呢??要回答这个问题,就必须等到下一节讲同步话题地时候了。

此外,关于互斥量相关的接口还有五个相关的细节问题:

问题1️⃣:加锁的原则问题

  • 由于互斥量的特性,同一时间段,有且只能有一个线程进入临界区,因此线程进入临界区后,就会由并行转为串行。这样必然会导致效率降低,而这样的操作又是无法避免的,因此,在实践中,加锁的粒度必须足够的细

问题2️⃣:mutex也是共享资源,那么谁来保护它呢🤔??

  • 在我们先前的测试代码当中,我们使用的时静态分配,即定义了一个全局变量,但是全局变量不就是一个典型的共享资源吗??所有的线程都可以去使用,既然它来保护别人,那么谁来保护它呢??
  • 实际上,当年互斥量的设计师们也考虑过这个问题,为了避免mutex还需要被保护,于是就将加锁和解锁操作设计为原子性的,也就是说加锁或者解锁这两个操作是一步到位的,并不会因为CPU的调度,而造成我们先前所讲的线程安全问题。

问题3️⃣:一些线程遵守先加锁再解锁,而有一些线程不遵守呢🤔??

  • 这种情况基本上是不会发生的,除非有人故意写bug。
  • 访问临界资源,所有的线程都必须遵守加锁和解锁的规则,绝不能有例外,这是一个共识。

问题4️⃣:申请锁失败的线程在做些什么呢🤔??

  • 一般多线程同时申请互斥量,但是只会有一个申请成功,没有竞争到互斥量的线程会在pthread_ lock陷入阻塞(执行流被挂起),等待互斥量解锁。

问题5️⃣:临界区内部也会有多行代码,那么会发生线程切换嘛🤔??

  • 当然,即使是执行到加锁解锁操作内部,也会发生线程切换。因为,临界区本质是人为规定的一个概念,而在CPU眼中,本质上都是代码。因此,不管是不是在临界区,都会发生线程切换。

1.4、互斥量的原理

首先,我们得了解一个知识点:如果一个语句只有一行汇编代码就可以表示,那么执行该语句就是原子的。或者简单来说,一条汇编语句操作是原子的💧

互斥锁的加锁解锁操作的汇编伪码如下图:

其中,lock的第一行就是将某一个寄存器存入0,而第二行则是整个加锁逻辑中最关键的一行!!,它将寄存器中的值与内存中的mutex值进行了交换!!

我们知道CPU在调度线程的时候,是以线程为载体执行的加锁逻辑。当寄存器的值与内存中mutex的值交换过后,原本mutex的值就归属于当前线程了,即使交换后立刻就发生了线程调度,该值也会变成硬件上下文一直跟着这个线程。而交换后的mutex在内存中存储的值则会一直为零,后来的线程执行到交换语句后也只会0与0交换,完全没有任何影响。

实际上,锁就是我们上文中所谈到的1!!exchange始终没有拷贝,也就是说始终只有一个1谁拥有这个1,谁就拥有这个锁!!

再回看先前的问题5️⃣:临界区内部也会有多行代码,那么会发生线程切换嘛🤔??
现在来看,当然不会,锁只有一份,只要线程1不还回来,锁就属于线程1私有,线程切换不影响。

综上,互斥锁的本质就是由1至0的过程,互斥的本质就是独占!!🌟🌟

除此以外,锁的实现方式是多种多样的,除了上述利用软件的方式实现互斥锁,我们还可以利用硬件方式。
互斥锁的存在,本质就是让临界区中只存在一个线程,那么如果当某一个线程执行到临界区的代码时,立刻就将时钟中断关闭,此时操作系统就无法再进行调度了,线程则无法切换,这样就无人能够打扰该线程执行临界区的代码了。

这个操作在逻辑上一定是行得通的,但是一定不建议这么做,时钟中断可以说是操作系统的灵魂,这种“触及灵魂”的事是具有极大风险的。
之所以说这种实现方式,只是想说明一个结论:锁的实现是多种多样的!!💧


1.5、互斥量的封装

C++中也有互斥量相关的接口,只不过是利用面向对象的方式将他们封装起来了。
现在,我们也利用面向对象的方式,将互斥量接口封装。

classMutex{public:Mutex(){pthread_mutex_init(&lock,nullptr);}voidLock(){pthread_mutex_lock(&lock);}voidunLock(){pthread_mutex_unlock(&lock);}~Mutex(){pthread_mutex_destroy(&lock);}private:pthread_mutex_t lock;};

此外,我们亦可以按照RAII风格进行进一步封装。

补充:RAII是C++中一种非常重要的编程惯用法,核心思想是:资源的获取与对象的初始化绑定,资源的释放与对象的析构绑定,从而利用 C++ 对象生命周期(构造、析构)的自动管理机制来安全、简洁地管理资源(如动态内存、文件句柄、锁、套接字等)。

#include<pthread.h>classMutex{public:Mutex(){pthread_mutex_init(&lock,nullptr);}voidLock(){pthread_mutex_lock(&lock);}voidunLock(){pthread_mutex_unlock(&lock);}~Mutex(){pthread_mutex_destroy(&lock);}private:pthread_mutex_t lock;};classLockGuard// RAII风格{public:LockGuard(Mutex&lock)//引用了传进来的参数:_lock(lock){_lock.Lock();}~LockGuard(){_lock.unLock();}private:Mutex&_lock;//引用的引用了};

我们依旧用抢票程序进行测试:


完🌄🌄🌄

http://www.zskr.cn/news/1376829.html

相关文章:

  • 2026最新诚信优选昆明市黄金回收白银回收铂金回收彩金回收门店TOP5实力排行榜+联系方式推荐 - 前途无量YY
  • 别再傻傻分不清!用示波器和电流钳实测Peak Hold喷油器驱动波形(附波形解读)
  • 2026最新诚信优选赤峰市黄金回收白银回收铂金回收彩金回收门店TOP5实力排行榜+联系方式推荐 - 前途无量YY
  • 从一次PR被拒说起:我是如何用Git Upstream优雅同步Gitee Fork仓库的
  • 2026最新诚信优选兰州市黄金回收白银回收铂金回收彩金回收门店TOP5实力排行榜+联系方式推荐 - 前途无量YY
  • 代码解密——色度抠图背后的图像处理
  • 鸿蒙数学 108 篇 第十三篇:两仪数理内涵:阴阳二元数学定义
  • 耦合振荡器模型解析MPI并行计算同步机制
  • 城通网盘直连解析完整指南:三步获取高速下载链接的免费方案
  • 【测试】软件测试必读:一文搞懂BUG的生命周期与管理技巧
  • 2026最新诚信优选廊坊市黄金回收白银回收铂金回收彩金回收门店TOP5实力排行榜+联系方式推荐 - 前途无量YY
  • 番茄小说下载器:打造你的专属离线图书馆 [特殊字符]
  • 解锁音乐自由:QMCDecode帮你一键解密QQ音乐加密格式
  • 数学解题器 · 使用说明
  • 【数据库篇|MySQL】事务
  • Lumafly:空洞骑士模组管理终极指南,三步告别依赖冲突烦恼
  • Seraphine:英雄联盟玩家的5大核心功能终极助手,一键提升游戏体验
  • NCMDump完整指南:专业解析网易云音乐NCM文件转换技术
  • 基于GJB 438C-2021的《系统/子系统设计说明(SSDD)》完整案例
  • FeHelper:一站式前端开发工具箱的完整指南
  • 终极Gofile批量下载器深度解析:高效自动化文件获取的完整技术指南
  • 状态机枚举应用框架
  • UVa 285 Crosswords
  • 番茄小说下载器:Rust架构下的多格式内容获取与处理系统深度解析
  • 全面战争:战锤3 2026官方正版最新版pc免费下载(看到请立即转存 资源随时失效)手机版通用
  • Linux线程控制:从用户态控制到内核级克隆全链路解析
  • OneMore:深度解析如何突破OneNote内容管理瓶颈的全能插件
  • 3步掌握视频转PPT:从数小时手动截图到3分钟智能提取
  • 百考通一键生成高校标准任务书框架
  • 解决方案:猫抓浏览器扩展 - 现代网页媒体资源捕获与流媒体解析的专业工具