【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.rhkb.cn/news/469384.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【GVN】AWZ算法

AWZ算法的例子依旧来自于RKS的这篇文章《Detecting Equalities of Variables: Combining Efficiency with Precision》。 上面两个图&#xff0c;进行的是如下图所示的循环结构的等价类计算。 为什么得到的结果不是上图而是下图呢&#xff1f;这里其实是因为用到的AWZ的算法…

HBuilder使用虚拟机

按文档的连接一直不成功 没找到Simulator&#xff0c;原来是因为我电脑之前没安装过虚拟机版本 安装模拟器Simulator | uni-app官网 找到settings,左下角安装需要的对应版本的虚拟机就好了&#xff0c;然后重启hb

ubuntu下安装 git 及部署cosyvoice(2)

上一节已经可以了一部分。这一节&#xff0c;主要是让他动起来。 1.第一个错误 (cosyvoice) duyichengduyicheng-computer:~/gitee/CosyVoice$ python webui.py Traceback (most recent call last): File "webui.py", line 17, in <module> import grad…

16S,18S引物覆盖度测试:SILVA和PR2

16S 进入网站&#xff1a;https://www.arb-silva.de/search/testprime/ 填写引物和错配阈值 结果进入&#xff1a;Inspect Results inTaxonomy Browser 18S 进入网站&#xff1a;Primer database 进入&#xff1a;Test your primer set 可选择感兴趣的物种group&#xff0c;红…

【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

middleware中间件概述

中间件定义 中间件&#xff08;middleware&#xff09;是基础软件的一大类&#xff0c;属于可复用软件的范畴。顾名思义&#xff0c;中间件处在操作系统、网络和数据库之上&#xff0c;应用软件的下层&#xff08;如图 15-1 所示&#xff09;​&#xff0c;也有人认为它应该属…

大模型 | 2024年中国智能算力行业白皮书 | 附PDF免费下载

智能算力&#xff0c;是数字经济发展的重要基础性资源。由于美国的科技禁运政策与国内人工智能技术差距&#xff0c;我国在实现智算资源完全国产化的道路上仍需努力。为了谋求可用算力资源在物理空间的释放和高效利用&#xff0c;国家层面持续推进“东数西算”工程&#xff0c;…

面试题之---解释一下原型和原型链

实例化对象 和普调函数一样&#xff0c;只不过调用的时候要和new连用&#xff08;实例化&#xff09;&#xff0c;不然就是一个普通函数调用 function Person () {} const o1 new Person() //能得到一个空对象 const o2 Person() //什么也得不到&#xff0c;这就是普通的…

面试:TCP、UDP如何解决丢包问题

文章目录 一、TCP丢包原因、解决办法1.1 TCP为什么会丢包1.2 TCP传输协议如何解决丢包问题1.3 其他丢包情况&#xff08;拓展&#xff09;1.4 补充1.4.1 TCP端口号1.4.2 多个TCP请求的逻辑1.4.3 处理大量TCP连接请求的方法1.4.4 总结 二、UDP丢包2.1 UDP协议2.1.1 UDP简介2.1.2…

飞凌嵌入式FET527N-C核心板现已适配Android 13

飞凌嵌入式FET527N-C核心板现已成功适配Android13&#xff0c;新系统的支持能够为用户提供更优质的使用体验。那么&#xff0c;运行Android13系统的FET527N-C核心板具有哪些突出的优势呢&#xff1f; 1、性能与兼容性提升 飞凌嵌入式FET527N-C核心板搭载了全志T527系列高性能处…

Java static静态变量 C语言文件读写

1. &#xff08;1&#xff09; public class test1 {public static void main(String[] args) {javabean1.teachername"jianjun";//直接在类调用&#xff0c;方便一点点javabean1 s1 new javabean1();s1.setName("liujiawei");s1.setAge(18);s1.setGend…

Linux驱动开发(4):Linux的设备模型

在前面写的驱动中&#xff0c;我们发现编写驱动有个固定的模式只有往里面套代码就可以了&#xff0c;它们之间的大致流程可以总结如下&#xff1a; 实现入口函数xxx_init()和卸载函数xxx_exit() 申请设备号 register_chrdev_region() 初始化字符设备&#xff0c;cdev_init函数…

MYSQL隔离性原理——MVCC

表的隐藏字段 表的列包含用户自定义的列和由系统自动创建的隐藏字段。我们介绍3个隐藏字段&#xff0c;不理解也没有关系&#xff0c;理解后面的undo log就懂了&#xff1a; DB_TRX_ID &#xff1a;6 byte&#xff0c;最近修改( 修改/插入 )事务ID&#xff0c;记录创建这条记…

鸿蒙next打包流程

目录 下载团结引擎 添加开源鸿蒙打包支持 打包报错 路径问题 安装DevEcoStudio 可以在DevEcoStudio进行打包hap和app 包结构 没法直接用previewer运行 真机运行和测试需要配置签名,DevEcoStudio可以自动配置, 模拟器安装hap提示报错 安装成功,但无法打开 团结1.3版本新增工具…

计算机毕业设计Python+大模型斗鱼直播可视化 直播预测 直播爬虫 直播数据分析 直播大数据 大数据毕业设计 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

【Vue】Vue3.0(二十)Vue 3.0 中mitt的使用示例

上篇文章 【Vue】Vue3.0&#xff08;十九&#xff09;Vue 3.0 中一种组件间通信方式-自定义事件 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Vue专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月11日12点23分 文章目录 一、mitt 在…

显示器接口种类 | 附图片

显示器接口类型主要包括VGA、DVI、HDMI、DP和USB Type-C等。 VGA、DVI、HDMI、DP和USB Type-C 1. 观察 VGA接口:15针 DP接口&#xff1a;在DP接口旁&#xff0c;都有一个“D”型的标志。 电脑主机&#xff1a;DP(D) 显示器&#xff1a;VGA(15针) Ref https://cloud.tenc…

什么是数据平台?10 个值得了解的大数据平台示例

目前尚不清楚普通的 “数据” 是何时变成了 “大数据”。后一个术语可能起源于 20 世纪 90 年代的硅谷推介会和午餐室。更容易确定的是数据在 21 世纪是如何爆炸式增长的&#xff08;据估计&#xff0c;到 2025 年&#xff0c;人类每天将产生 463 EB的数据&#xff09;&#xf…

2024最新版JavaScript逆向爬虫教程-------基础篇之Chrome开发者工具学习

目录 一、打开Chrome DevTools的三种方式二、Elements元素面板三、Console控制台面板四、Sources面板五、Network面板六、Application面板七、逆向调试技巧7.1 善用搜索7.2 查看请求调用堆栈7.3 XHR 请求断点7.4 Console 插桩7.5 堆内存函数调用7.6 复制Console面板输出 工欲善…

Local Dimming和Mini LED简介

文章目录 Local Dimming和Mini LED的介绍区别和联系联系区别总结 Local Dimming和Mini LED的介绍 电视显示技术中的Local Dimming和Mini LED都是用于提升画面质量的背光技术&#xff0c;主要目的是增强对比度和改善黑色表现。以下是对它们的详细介绍&#xff1a; Local Dimmin…