【Linux系统编程】第四十五弹---线程互斥:从问题到解决,深入探索互斥量的原理与实现

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、线程互斥

1.1、见一见多线程访问问题

1.2、解决多线程访问问题

1.2.1、互斥量的接口

1.2.2、互斥量接口的使用

1.2.3、原理角度理解锁

1.2.4、实现角度理解


1、线程互斥

多个线程能够看到的资源 -- 共享资源   -> 我们需要对这部分资源进行保护(互斥同步)! 

1.1、见一见多线程访问问题

此处实现一个抢票的代码来看看多线程访问的问题!!!

模拟抢票,总票数一万张,总共四个线程进行抢票!!!

抢票函数 

void route(const std::string& name)
{while(true){// 有票才抢if(tickets > 0){usleep(1000); // 1ms -> 抢票花费时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;}else{break;}}
}

主函数

// 模拟抢票 10000张
int tickets = 10000;int main()
{// 1.创建4个线程Thread t1("thread-1",route);Thread t2("thread-2",route);Thread t3("thread-3",route);Thread t4("thread-4",route);// 2.启动4个线程t1.Start();t2.Start();t3.Start();t4.Start();// 3.终止4个线程t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

运行结果 

抢票的代码确实执行完了,但是有一个问题,就是最终的票数竟然是负数,正常的逻辑是票数为0就不能再抢票了,这是为什么呢? 

  • 计算机的运算类型: 算术运算 逻辑运算
  • CPU内,寄存器只有一套,但是寄存器里面的数据可以有多套
  • 寄存器里面的数据看起来放在了一套公共的寄存器中,但是属于线程私有,当它被切换的时候,线程要带走自己的数据!回来的时候恢复数据 
  • 当线程2(tickets = 1)判断之后,线程被切换了两次,此时前面两个线程的数据会被保存,当当前线程执行完会继续执行前面的代码,次数就会出现最终tickets = -2的情况!!!

运行结果  

1.2、解决多线程访问问题

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

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

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

1.2.1、互斥量的接口

初始化互斥量

  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量attr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

1.2.2、互斥量接口的使用

1、全局锁(方法传name)

锁是全局的或者静态的,只需init,不需要destory。

抢票函数

// 模拟抢票 10000张
int tickets = 10000; // 共享资源,造成数据不一致问题// 全局或者静态只需INIT,无需destory
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;void route(const std::string &name)
{while (true){pthread_mutex_lock(&gmutex); // 上锁// 有票才抢if (tickets > 0){usleep(1000); // 1ms -> 抢票花费时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;pthread_mutex_unlock(&gmutex); // 解锁}else{pthread_mutex_unlock(&gmutex); // 退出循环之前解锁,否则会阻塞break;}}
}

主函数

static int threadnum = 4;int main()
{std::vector<Thread> threads;// 1.创建线程for (int i = 0; i < threadnum; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(name, route);}// 2.启动线程for (auto &thread : threads){thread.Start();}// 3.终止线程for (auto &thread : threads){thread.Join();}return 0;
}

运行结果  

当我们运行程序的时候,可以明显看到抢票的过程变慢了,且最后抢到只有一张票了。

  • 所谓的对临界资源进行保护,本质是对临界区代码进行保护!
  • 我们对所有资源进行访问,本质都是通过代码进行访问的!

运行结果  

解决历史问题:

  • 1、加锁的范围,粒度一定要小(代码行数要少)
  • 2、任何线程要进行抢票,都得先申请锁,原则上不应该有例外
  • 3、所以线程申请锁,前提是所有线程都看到这把锁,锁本身也是共享资源 --- 加锁的过程,必须是原子的
  • 4、原子性:要么不做,要做就做完,没有中间状态
  • 5、如果线程申请锁失败,线程要被阻塞
  • 6、如果线程申请锁成功,线程继续往后运行
  • 7、如果线程申请成功了,执行临界区的代码,执行临界区代码期间,可以切换?
    • 可以切换,其他线程无法进入!因为虽然线程被切换了,但是没有释放锁!
      • 所以线程可以放心的执行完毕,没有线程能打扰!

结论:

对于其他线程,要么我没有申请锁,要么我释放了锁,对其他线程才有意义!! -> 线程访问临界区,对于其他线程是原子的。

2、局部锁(方法传结构体)

1、局部锁需要init且需要destory。

2、方法的参数调整成结构体之后,需要在原始的线程类加结构体成员变量,且修改构造函数

3、需要函数指针类型

4、需改Excute函数

ThreadData类及函数指针

class ThreadData
{
public:ThreadData(const std::string& name,pthread_mutex_t* lock):_name(name),_lock(lock){}
public:std::string _name;pthread_mutex_t* _lock;
};typedef void (*func_t)(ThreadData* td); // 函数指针类型

Thread类

class Thread
{
public:void Excute(){std::cout << _name << " is running" << std::endl;_isrunning = true;_func(_td);_isrunning = false;}
public:Thread(const std::string& name,func_t func,ThreadData* td):_name(name),_func(func),_td(td){std::cout << "create " << _name << " done" << std::endl;}
private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程要执行的回调函数ThreadData* _td;
}

抢票函数

// 模拟抢票 10000张
int tickets = 10000; // 共享资源,造成数据不一致问题void route(ThreadData* td)
{while (true){pthread_mutex_lock(td->_lock); // 上锁// 有票才抢if (tickets > 0){usleep(1000); // 1ms -> 抢票花费时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;pthread_mutex_unlock(td->_lock); // 解锁}else{pthread_mutex_unlock(td->_lock); // 退出循环之前解锁,否则会阻塞break;}}
}

主函数

static int threadnum = 4;int main()
{pthread_mutex_t mutex; // 局部锁,需要init和destorypthread_mutex_init(&mutex, nullptr);std::vector<Thread> threads;// 1.创建线程for (int i = 0; i < threadnum; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData *td = new ThreadData(name, &mutex); // new一个Thread对象,传局部锁threads.emplace_back(name, route,td);}// 2.启动线程for (auto &thread : threads){thread.Start();}// 3.终止线程for (auto &thread : threads){thread.Join();}pthread_mutex_destroy(&mutex);return 0;
}

运行结果  

局部锁与全局锁的结果完全相同,都能正常完成任务!!! 

3、RAII锁(构造上锁析构解锁)

LockGuard类

class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t* _mutex;
};

抢票函数

// 3.RAII
int tickets = 10000; // 共享资源,造成数据不一致问题void route(ThreadData* td)
{while (true){LockGuard lockguard(td->_lock); // RAII锁风格// 有票才抢if (tickets > 0){usleep(1000); // 1ms -> 抢票花费时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;}else{break;}}
}

运行结果  

1.2.3、原理角度理解锁

互斥锁

  • 原理:互斥锁用于保护临界区(Critical Section),确保同一时刻只有一个线程可以进入临界区。
  • 实现:通常通过操作系统的内核提供的原子操作(如CAS,Compare-And-Swap)来实现。

1.2.4、实现角度理解

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

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

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

相关文章

【贪心算法】贪心算法三

贪心算法三 1.买卖股票的最佳时机2.买卖股票的最佳时机 II3.K 次取反后最大化的数组和4.按身高排序5.优势洗牌&#xff08;田忌赛马&#xff09; 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#…

认知战认知作战:雍正设立军机处的认知战策略分析

认知战认知作战&#xff1a;雍正设立军机处的认知战策略分析 认知战认知作战&#xff1a;雍正设立军机处的认知战策略分析 认知战认知作战&#xff1a;雍正设立军机处的认知战策略分析 关键词&#xff1a;认知作战,新质生产力,人类命运共同体,认知战,认知域,认知战研究中心,认…

AI - 人工智能;Open WebUI;Lobe Chat;Ollama

AI - 人工智能&#xff1b;Ollama大模型工具&#xff1b;Java之SpringAI&#xff08;三&#xff09; 鉴于使用Ollama的命令行交互不是很方便&#xff0c;所以我们需要另一个开源的WebUI&#xff0c;搭建一个能通过浏览器访问的界面&#xff1b;Ollama的Web & Desktop非常多…

特斯拉车速转向电量充电油门刹车档位车门座椅调节灯光信号采集

特斯拉CAN信号采集方案主要包括硬件连接、数据采集与配置、数据解析与可视化等步骤。速锐得主要采集和测试关于特斯拉车速转向电量充电油门刹车档位车门座椅调节灯光信号。 我们拆开特斯拉网关部分用于CAN总线的连接&#xff0c;将CANH和CANL接入到网关对应的CAN针脚&#xff0…

【LLM Agents体验 3】利用Open-WebUI+Ollama本地部署Qwen2.5:7B大模型的安装指南

Open WebUI是一种基于 Web 的用户界面&#xff0c;用于管理和操作各种本地和云端的人工智能模型。它提供了一个直观的图形化界面&#xff0c;使用户可以方便地加载、配置、运行和监控各种 AI 模型&#xff0c;而无需编写代码或使用命令行界面。 Open-WebUI 是一款功能强大且易于…

3.2 Fiddler基础测试

1 请求响应报文 1.1 请求部分 Headers&#xff1a;显示请求的头信息&#xff0c;重点关注请求类型。textView & SyntaxView&#xff1a;分别以纯文本和语法高亮的方式显示请求参数。WebForms&#xff1a;显示请求的 GET 参数和 POST body 内容。HexView&#xff1a;以十六…

《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项

《TCP/IP网络编程》学习笔记 | Chapter 9&#xff1a;套接字的多种可选项 《TCP/IP网络编程》学习笔记 | Chapter 9&#xff1a;套接字的多种可选项套接字可选项和 I/O 缓冲大小套接字多种可选项getsockopt & setsockoptSO_SNDBUF & SO_RCVBUF SO_REUSEADDR发生地址绑定…

ISAAC SIM踩坑记录--ROS2相机影像发布

其实这个例子官方和大佬NVIDIA Omniverse和Isaac Sim笔记5&#xff1a;Isaac Sim的ROS接口与相机影像、位姿真值发布/保存都已经有详细介绍了&#xff0c;但是都是基于ROS的&#xff0c;现在最新的已经是ROS2&#xff0c;这里把不同的地方简单记录一下。 搭建一个简单的场景&a…

【C++】模板(一):函数模板

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解C的函数模板&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 1&#xff0e;模板2. 函数模板1 概念2 函数模板的实例化(A) 隐式实例化&#xff1a;让编译…

通俗易懂的介绍期权如何开户以及条件与规则!

一文带你了解通俗易懂的介绍期权如何开户以及条件与规则&#xff0c;一般在证券和期货开通期权账户是需要满足基本的验资门槛&#xff0c;而期权平台是可以在线上开通的。具体的期权开户过程可以参考以下几个步骤。 一、期权开户的条件和流程 1. 年龄要求&#xff1a;投资者必…

【go从零单排】Text Templates

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 Go 中的文本模板&#xff08;Text Templates&#xff09;是通过 text/template 包…

mysql 常用命令(二)

1、创建空表 mysql> CREATE TABLE test ( id int(4) NOT NULL AUTO_INCREMENT, name char(20) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETlatin1;AUTO_INCREMENT&#xff1a;自增&#xff0c;下次插入数据&#xff0c;会自动增加ID的值&#xff0c;…

麦肯锡数字化转型方法论:系统性四步法

在全球范围内&#xff0c;众多企业纷纷投身于数字化转型&#xff0c;对价值链各环节产生深远影响。 数字化转型不仅能够创造价值&#xff0c;提高生产力&#xff0c;还能帮助企业保持竞争优势。 然而&#xff0c;大多数转型都因准备不充分而遭遇失败。 麦肯锡通过对全球上千个…

政企学习考试系统(源码+文档+部署+讲解)

本文将深入解析“政企学习考试系统”的项目&#xff0c;探究其架构、功能以及技术栈&#xff0c;并分享获取完整源码的途径。 系统概述 本项目名称为政企学习考试系统&#xff0c;是一款面向政企用户的在线学习与考试平台。该系统旨在为用户提供便捷、高效的学习和考核服务&a…

Jmeter基础篇(22)服务器性能监测工具Nmon的使用

一、前言 我们在日常做压测的过程中&#xff0c;不仅仅需要监控TPS&#xff0c;响应时间&#xff0c;报错率等这些系统基础性能数据&#xff0c;还需要对服务器的性能&#xff08;如CPU、磁盘、内存、网络IO等&#xff09;做监控&#xff0c;以求对系统运行过程中的硬件性能有…

三级等保安全解决方案,实施方案,整改方案(Word,PPT等相关资料学习)

信息系统进行三级等保的主要原因在于保障信息安全&#xff0c;维护国家安全和公共利益。三级等保是我国根据相关法律法规制定的信息安全等级保护制度中的一部分&#xff0c;旨在确保信息系统的完整性、可用性和保密性。通过三级等保&#xff0c;信息系统可以得到一系列的安全保…

Springboot 使用EasyExcel导出含图片并设置样式的Excel文件

Springboot 使用EasyExcel导出含图片并设置样式的Excel文件 Excel导出系列目录&#xff1a;★★★★尤其注意&#xff1a;引入依赖创建导出模板类逻辑处理controllerservice 导出效果总结 Excel导出系列目录&#xff1a; 【Springboot 使用EasyExcel导出Excel文件】 【Springb…

老破机器硬盘要坏,转移虚拟机里的打字平台过程全记录

我有一台老破机器&#xff0c;说破其实没多破&#xff0c;但是老应该是有几年了&#xff0c;这机器一开始一直放在领导办公室不让人用&#xff0c;后来因为单位整体搬迁&#xff0c;我跟领导讨了来&#xff0c;win10的系统&#xff0c;我装了个虚拟机好像是15的版本&#xff0c…

光流法与直接法在SLAM中的应用

本文总结视觉SLAM中常用的光流法与直接法 1、Lucas-Kanade光流法 相机所拍摄到的图像随相机视角的变化而变化&#xff0c;这种变化也可以理解为图像中像素的反向移动。“光流”&#xff08;Optical Flow&#xff09;是指通过分析连续图像帧来估计场景中像素或特征点的运动的技…

VPN相关学习笔记

目录 VPN IPSec AH ESP IKE 工作流程 SSL SSL协议 握手协议 记录协议 警告协议 非对称密钥协商过程 SSL VPN工作 两种技术对比 VPN 介绍&#xff1a;VPN创建了一个专用隧道&#xff0c;用于安全地传输数据。Internet协议安全&#xff08;IPSec&#xff09;和安全套…