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

线程互斥与同步(上)

目录

线程互斥

互斥相关背景概念

 互斥量 mutex

什么情况下会引发线程互斥?

解决方案:

锁原理

死锁问题

 其他


线程互斥


互斥相关背景概念

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

 互斥量 mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量

  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互

  • 多个线程并发的操作共享变量,会带来一些问题

什么情况下会引发线程互斥?

如下代码,很简单的买票系统

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <unistd.h>
class TicketSystem
{
public:TicketSystem(int total) : tickets(total) {}void sellTicket(){while (true){if (tickets > 0){// 模拟处理时间std::this_thread::sleep_for(std::chrono::milliseconds(10));tickets--;std::cout << "售出1张票,剩余: " << tickets<< " (线程ID: " << std::this_thread::get_id() << ")\n";//usleep(10);//稍做等待防止单一线程独占锁}else{break;}}}int getRemaining(){return tickets;}private:int tickets;
};int main()
{const int TOTAL_TICKETS = 100;const int THREAD_NUM = 10;TicketSystem system(TOTAL_TICKETS);std::vector<std::thread> threads;for (int i = 0; i < THREAD_NUM; ++i){threads.emplace_back([&system](){ system.sellTicket(); });}for (auto &t : threads){t.join();}std::cout << "最终剩余票数: " << system.getRemaining() << std::endl;return 0;
}

 运行结果

可以看到票出现了负数

这里其实就是临界区的非原子性问题,

对于其中的ticket--时,在进行汇编指令后会变成

// 取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 	40064b: 	8b 05 e3 04 20 00   mov 	0x2004e3(%rip),%eax 	# 600b34 <ticket>
153 	400651: 	83 e8 01 			sub 	$0x1,%eax
154 	400654: 	89 05 da 04 20 00   mov 	%eax,0x2004da(%rip) 	# 600b34 <ticket>

-- 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

 在其运行时可能会发生如下错误

也就是说线程在运行ticket--指令时如果在其中汇编代码第三步之前(也就是写回共享变量ticket的内存地址的步骤之前)由于时间片用完等原因被挂起,在原线程中就没有对内存中的tickets的值进行写回,如果此时后面的线程进行修改内存但是不论怎么修改,最后都会被重新执行的原线程给覆盖,比如如果原线程本来是要将99写到内存但是在最后一步被挂起,而后面的线程已经将内存给修改为1,但是如果此时原线程重新执行将又会把值写回内存变成99,是其他进程的努力付之东流。

哦哦哦~,但是这里还有一个问题啊,为什么会有负数的出现呢,这里其实是逻辑运算的问题,tickets > 0这也是非原子性的,如果在判读为true后进行线程切换将会造成后面的线程已经将内存里的ticket改变为负数或0但是原线程重新执行时因为已经判断过造成ticket的负数出现.

所以为了避免这种情况加入了锁的概念

解决方案:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量!

互斥量用于多线程编程中的同步机制,用于防止多个线程同时访问共享资源,从而避免数据竞争和不一致性。其主要目的是保证在任何时刻,只有一个线程可以访问特定的资源或代码段

 

互斥锁的工作原理:

  • 初始化:在互斥锁被使用之前,需要对其进行初始化。这通常是在创建线程之前完成的
  • 加锁(Lock):当一个线程需要访问共享资源时,它会尝试获取互斥锁。如果锁当前未被其他线程持有,则该线程将成功获取锁,并继续执行其后续代码。如果锁已被其他线程持有,则该线程将被阻塞,直到锁被释放为止
  • 临界区:持有互斥锁的线程可以安全地访问共享资源或执行临界区代码。临界区是指那些需要同步访问的代码段
  • 解锁(Unlock):当线程完成对共享资源的访问后,它会释放互斥锁。这允许其他被阻塞的线程获取锁并访问共享资源
  • 销毁:在不再需要互斥锁时,可以将其销毁。这通常是在程序结束或线程终止时进行的
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <unistd.h>
class TicketSystem
{
public:TicketSystem(int total) : tickets(total) {}void sellTicket(){while (true){pthread_mutex_lock(&mutex); // 加锁 阻塞其他线程if (tickets > 0){// 模拟处理时间std::this_thread::sleep_for(std::chrono::milliseconds(10));tickets--;std::cout << "售出1张票,剩余: " << tickets<< " (线程ID: " << std::this_thread::get_id() << ")\n";pthread_mutex_unlock(&mutex); // 解锁//usleep(10);//稍做等待防止单一线程独占锁}else{pthread_mutex_unlock(&mutex); // 解锁break;}}}int getRemaining(){return tickets;}private:int tickets;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 通过宏 PTHREAD_MUTEX_INITIALIZER 快速初始化一个默认属性的互斥锁
};int main()
{const int TOTAL_TICKETS = 100;const int THREAD_NUM = 10;TicketSystem system(TOTAL_TICKETS);std::vector<std::thread> threads;for (int i = 0; i < THREAD_NUM; ++i){threads.emplace_back([&system](){ system.sellTicket(); });}for (auto &t : threads){t.join();}std::cout << "最终剩余票数: " << system.getRemaining() << std::endl;return 0;
}

  • 静态初始化:通过宏 PTHREAD_MUTEX_INITIALIZER 快速初始化一个默认属性的互斥锁。

  • 作用:锁的初始状态为「未锁定」(unlocked)。

 很好我们可以返现我们解决了线程并发的问题,但是又出现了一个新的问题怎么全是一个线程在执行呢,这里是因为这是一开始是原线程先拿到锁抢到票,但是之后原线程对锁的竞争力就会远强于其它线程了,因为它将锁刚一释放就马上又获取了,所以我们采取的方法可以是:原线程2完票之后可以让它短暂睡眠一会儿,这样其它线程就能够来争夺锁了,放开注释即可。

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

互斥锁原理

  • 第一步:首先将al寄存器清零
  • 第二步:将al寄存器中的内容和mutex的内容做一次交换,这个动作其实就是我们申请锁的动作
  • 第三步:判断,如果锁的数量大于0就会申请成功并退出,如果不大于0就会挂起等待

在整个加锁的过程中只有一个数值1存在,这个1要么保存在某个线程的私有的寄存器中,要么保存在共享的互斥锁变量mutex里。如果某个线程的寄存器中的值为1,说明该线程已经加锁成功。

锁本身其实也是作为共享资源存在的,那锁本身需要保护吗?

答案是不需要,因为这个锁是互斥的

  • 任何时候只有一个线程能成功将 mutex 从 1 改为 0,其他线程必须等待。

死锁问题

死锁(Deadlock)是指多个线程或进程因互相等待对方释放资源而陷入无限阻塞的状态。常见的死锁原因包括:

  • 互斥条件:资源一次只能被一个线程占用(如锁)。

  • 占有并等待:线程持有至少一个资源,同时等待获取其他资源。

  • 非抢占条件:已分配给线程的资源不能被强制剥夺,只能由线程主动释放。

  • 循环等待:多个线程形成环形等待链(如 T1 等待 T2 的资源,T2 等待 T1 的资源)。

 其他

互斥量的加锁和解锁操作会带来一定的性能开销,尤其是在高并发场景下。为了减少开销,可以:

  • 尽量减少临界区的范围;

  • 使用读写锁(std::shared_mutex)替代互斥量;

  • 使用无锁数据结构(Lock-Free Data Structures)。

 

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

相关文章:

  • 2025年渗透测试面试题总结-拷打题库36(题目+回答)
  • Python Cookbook-6.18 用__init__参数自动初始化实例变量
  • 多端定制系统开发:打造高效全平台覆盖的APP解决方案
  • Python爬虫(16)Python爬虫数据存储新维度:Redis Edge近端计算赋能实时数据处理革命
  • phpyun人才系统v7.0升级v7.1 开源vip版,php云专业人才招聘系统小程序零工市场源码支持v4.6的更新步骤流程详解
  • 工作记录 2015-07-15
  • 数据结构的基本概念以及算法的基本内容
  • python:如何获取股票 周K线数据、月K线数据
  • Go语言入门基础:协程
  • 【信息系统项目管理师-论文真题】2012上半年论文详解(包括解题思路和写作要点)
  • 装饰器@wraps(func)详解
  • 伊甸园之东: 农业革命与暴力的复杂性
  • Learning vtkjs之Cutter
  • 有向图强连通分量好题分享(一)
  • 【深度学习的灵魂】图片布局生成模型LayoutPrompt(2)·布局序列化模块
  • 如何通过文理工三类AI助理赋能HI,从而,颠覆“隔行如隔山”的旧观念和“十万小时定律”的成长限制
  • 快速掌握--cursor
  • 深入解析 Stacking:集成学习的“超级英雄联盟
  • 排查 EF 保存数据时提示:Validation failed for one or more entities 的问题
  • 以梦为舟,共赴中医星辰大海
  • 【Mytais系列】Type模块:源码
  • MySQL快速入门篇---数据库约束
  • 【计算机视觉】三维重建: MVSNet:基于深度学习的多视图立体视觉重建框架
  • 驱动精灵v9.7(含网卡版)驱动工具软件下载及安装教程
  • 360驱动大师v2.0(含网卡版)驱动工具软件下载及安装教程
  • The Traitor King (10 player 25 player)
  • 【网络编程】HTTP(超文本传输协议)详解
  • 文献总结:TPAMI端到端自动驾驶综述——End-to-End Autonomous Driving: Challenges and Frontiers
  • 《解锁Windows下GCC升级密码,开启高效编程新旅程》
  • 游戏引擎学习第255天:构建配置树