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

Linux操作系统之线程:线程互斥

前言

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

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

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

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

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

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

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

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

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


二、互斥量mutex

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

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

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

代码语言:javascript

AI代码解释

#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库中自然也有相应的调用接口:

初始化:

代码语言:javascript

AI代码解释

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

代码语言:javascript

AI代码解释

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

互斥锁销毁:

代码语言:javascript

AI代码解释

int pthread_mutex_destroy(pthread_mutex_t *mutex);

加锁与解锁:

代码语言:javascript

AI代码解释

int pthread_mutex_lock(pthread_mutex_t *mutex);

代码语言:javascript

AI代码解释

int pthread_mutex_unlock(pthread_mutex_t *mutex);

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

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

  • mutex:指向要初始化的互斥锁。
  • attr:锁的属性(NULL表示默认属性)。

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

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

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

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

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

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

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

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

代码语言:javascript

AI代码解释

#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 提供了原子交换指令(如swapexchange):

  • 原子交换指令的作用: 将寄存器内存单元的数据进行交换,由于该操作是单条 CPU 指令,因此具有原子性。
  • 多处理器环境下的保证: 即使多个 CPU 核心同时访问同一内存地址,总线仲裁机制也会确保同一时刻只有一个swap指令能执行,其他 CPU 必须等待。

3.lockunlock的底层实现(伪代码)

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

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

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

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

没错,是xchgb %al ,mutex


四、封装mutex

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

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

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

代码语言:javascript

AI代码解释

#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代码就可以变成:

代码语言:javascript

AI代码解释

#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类(临时变量生命周期)来帮助我们进行管理,如果手动进行解锁上锁,难免会出现遗漏。

www.dongchedi.com/article/7602309546480304664
www.dongchedi.com/article/7602310381637763609
www.dongchedi.com/article/7602310463925797401
www.dongchedi.com/article/7602307905395950104
www.dongchedi.com/article/7602307747287368216
www.dongchedi.com/article/7602305637967888958
www.dongchedi.com/article/7602306631019479614
www.dongchedi.com/article/7602305597303833150
www.dongchedi.com/article/7602305394706563608
www.dongchedi.com/article/7602305863474233880
www.dongchedi.com/article/7602304270255309374
www.dongchedi.com/article/7602305877340815934
www.dongchedi.com/article/7602303679164940825
www.dongchedi.com/article/7602304716139725337
www.dongchedi.com/article/7602304133735121432
www.dongchedi.com/article/7602305306105774654
www.dongchedi.com/article/7602303734014099993
www.dongchedi.com/article/7602304547343581758
www.dongchedi.com/article/7602304119059497497
www.dongchedi.com/article/7602301951069471294
www.dongchedi.com/article/7602303850267460120
www.dongchedi.com/article/7602302937162957374
www.dongchedi.com/article/7602284819506250302
www.dongchedi.com/article/7602285111878648382
www.dongchedi.com/article/7602284187542274584
www.dongchedi.com/article/7602285208242586136
www.dongchedi.com/article/7602282650446987801
www.dongchedi.com/article/7602280859521335870

http://www.jsqmd.com/news/335756/

相关文章:

  • 2026年口碑好的海绵胶辊行业内知名厂家推荐 - 行业平台推荐
  • 2026年2月GEO优化公司权威榜:技术和营销能力TOP5深度测评
  • 销冠一走,客户全跑?2026年,你的销售团队该彻底换种活法了!
  • CAXA编程方案覆盖全工艺类型
  • 从步态分析到康复医学:青瞳视觉(CHINGMU)如何用高精度动捕解读人体“运动密码”
  • <span class=“js_title_inner“>SolarWinds 修复四个严重漏洞,可导致未认证RCE和认证绕过</span>
  • 2026年驻马店复合肥厂家综合评测与选型指南 - 2026年企业推荐榜
  • 2026年比较好的面板式流量计/塑料转子流量计厂家怎么选 - 行业平台推荐
  • AI原生应用中的上下文理解:常见误区与解决方案
  • Step-Audio-R1:语音模态的Scaling Law
  • 把你的MCP Server部署到公网,让阿里云上的应用来访问和使用
  • AI培训海外就业机构实测对比:3家主流机构深度测评,避坑指南+理性选择建议 - 短商
  • 基于SpringBoot+Vue的和餐饮管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • 2026年口碑好的高精度流量计量仪表厂家用户好评推荐 - 行业平台推荐
  • 基于SpringBoot+Vue的spring boot疫情信息管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • 驻马店农资选购指南:如何甄别真正专业的化肥与农资服务商? - 2026年企业推荐榜
  • 深入研究大数据领域 Hadoop 的 Oozie 工作流调度系统
  • 2026年温州优质激光笔厂商甄选指南与深度评测 - 2026年企业推荐榜
  • 武昌区优质英语教学辅导班盘点与选择建议 - 2026年企业推荐榜
  • 武汉东湖高新区幼儿英语辅导班选择指南与机构推荐 - 2026年企业推荐榜
  • IDEA 报错 TS7016: Could not find a declaration file for module xxxx
  • <span class=“js_title_inner“>《Docker极简教程》--Docker网络--Docker网络的配置和使用</span>
  • 2026年质量好的流量计量仪表/面板式流量计量仪表厂家实力与用户口碑参考 - 行业平台推荐
  • Orthogonal Subspace Decomposition for Generalizable AI-Generated ImageDetection
  • 基于SpringBoot+Vue的智慧校园之家长子系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • <span class=“js_title_inner“>简单聊聊在SQL Server 中索引对like语句到底有没有帮助</span>
  • <span class=“js_title_inner“>国家基因组科学数据中心颁发2025年度“最佳数据共享奖”与“高影响力数据奖”</span>
  • 企业级毕业论文管理系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】
  • 01-YOLO最新版到底新在哪
  • 【毕业设计】SpringBoot+Vue+MySQL 和餐饮管理系统平台源码+数据库+论文+部署文档