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

【Linux系统】深入理解线程,互斥及其原理

前言:

        上文我们讲到了线程的概念与控制【Linux系统】初见线程,概念与控制-CSDN博客

        本文我们再来讲讲,线程的下一部分内容:线程的互斥与同步!
        点个关注!

线程互斥

        在上一文章中我们讲到了线程的概念与控制,知道了线程是共享进程中的绝大部分资源的!但是当多个线程同时访问同一个公共资源时,就会出现一个问题:数据不一致!

        为解决数据不一致问题,我们就想要再深入理解线程中的互斥与同步!

互斥相关概念

        临界资源:多线程执行流中被保护的共享资源就叫做临界资源

        临界区:每个线程内部,访问临界资源的代码区,就叫做临界区

        互斥:对临界资源起保护作用!保证任何时候,有且只有一个执行流进入临界区,访问临界资源!

        原子性:表示一个操作拥有两态:要么完成,要么没做,不存在做了一半的情况!这就表示这个操作拥有原子性不会被任何调度打断当前任务进行的性质!

互斥

数据不一致现象
//模拟抢票流程#include <iostream>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <pthread.h>
using namespace std;int ticket = 1000;void *route(void *args)
{string name = static_cast<char *>(args);while (true){if (ticket > 0){usleep(1000);cout << name << ":sells ticket:" << ticket << endl;ticket--;}elsebreak;}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

        我们发现,票竟然被抢到了负数!可明明我们的判断条件是 ticket > 0 啊?

        这就是多线程下,数据不一致的问题!也叫做线程安全问题!

用锁解决数据不一致问题
// 用锁解决,数据不一致问题(pthread锁)
int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义并初始化锁void *route(void *args)
{string name = static_cast<char *>(args);while (true){// 对临界区上锁pthread_mutex_lock(&lock);if (ticket > 0){usleep(1000);cout << name << ":sells ticket:" << ticket << endl;ticket--;// 结束访问临界区,解锁pthread_mutex_unlock(&lock);}else{// 结束访问临界区,解锁pthread_mutex_unlock(&lock);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

        通过锁的方式,对临界区进行保护,解决数据不一致的问题!

理解为什么数据会不一致

        首先,我们要知道ticket--操作,对于计算机来说并不是原子的!为什么呢?因为ticket--的操作对于计算机来说并不是一个步骤,而是3个步骤。

        一条汇编语句当然是原子的,因为只存在做了或者没做的情况。但多条的汇编语言,一般都是不具有原子性的!正如图中的ticket--操作一样!

        ticket--操作分为3步:第一步,将ticket的值加载到CPU的运行寄存器ebx中。第二部,运算寄存器ebx进行减一操作,将1000减为999。第三步,将ebx中的内容写回至内存中的ticket。由此完成了ticket--操作。

        理解了ticket--操作不是原子的,下面我们再来看看:

        我们都知道线程是有时间片的,当时间耗尽时,线程会保存上下文数据,并被剥离出CPU

        那么,当一个线程执行上述代码时。因为ticket--操作不是原子的,所以ticket--操作的3个步骤没执行完就被剥离走是常见的情况!那么当一轮执行完成后,没有执行完的线程又会回来继续执行,但没有执行完的线程的上下文数据会直接覆盖CPU中的寄存器,接着执行。这意味着,在这个线程之前的线程所做的一切都没有意义,会被当前这个线程之间覆盖掉!!!

        举例理解:假设线程有一线程a,执行到运算寄存器ebx减1后,时间片耗尽线程的上下文数据被保存后被剥离!新的线程进来,执行线程操作。假设后续代码一切顺利执行,成功的将ticket减到了900。当旧线程重新被CPU调度时,旧线程的上下文数据覆盖CPU中的寄存器数据,继续执行还没有执行的代码,此时旧线程已经将运算寄存器ebx中的内容减1了,下一步将ebx的中内容写回ticket中!此时ticket中的900被覆盖为999!着也就意味着之前的线程说做的事是无意义的了!

        综上所述,这个就是数据不一致问题的原因!

理解为什么会变成负数

        在多线程中,存在多个线程同时通过if判断,进入临界区中,访问临界资源的情况!

        那如果当ticket此时为1,被多个线程执行减一操作,那ticket不就变为负数了吗!!!

        所以在此代码中,存在严重问题!数据不一致问题 + "超售"。

认识锁的作用

        锁的全称:互斥锁(又叫互斥量)

        顾名思义,互斥锁。其作用就是互斥!保护临界区资源,保证同一时间有且只有一个线程可以进入临界区,访问临界资源!杜绝数据不一致问题!

锁接口(pthread库)
初始化互斥锁
全局初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t是数据类型
PTHREAD_MUTEX_INITIALIZER是宏全局申请互斥锁,代码结束会自动释放
局部初始化锁:pthread_mutex_t mutex;int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);参数二:为属性指针,默认传nullptr
注意:局部初始化的锁想要我们自己手动的销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);
申请锁与解锁
申请锁:
将已经初始化后的锁,传入给pthread_mutex_lock进行申请锁int pthread_mutex_lock(pthread_mutex_t* mutex)
解锁:
线程获得锁后必须解锁,保证后续的线程也可以进入临界区,访问临界资源int pthread_mutex_unlock(pthread_mutex_t* mutex);

        申请成功:线程进入临界区,访问临界资源

        申请失败:线程挂起,等待下一次的申请

        锁提供的作用:将执行临界区的代码由并行转化为串行!保证线程执行期间不会被打扰。

理解:

如果线程不遵守锁的规则呢?

        这将是一个bug!所有线程都必须遵守!

加锁之后,在临界区线程进行了切换会怎么样?

        不怎么样。因为锁是被线程持有的,线程被切换了其申请的锁并没有解锁!这就意味着其他线程必须等待被切换的线程回来继续执行,直到退出临界区解锁。

形象理解:

        现在有一个自习室,只能容纳一个人进入其中。

        大家都来争抢锁,只有获得锁的人才能够进入自习室!在你获得锁的期间,没有任何人没有进入自习室,必须等待你归还钥匙,解锁后。再次获得锁的人才能够进入自习室。

        在你获得锁的期间,即使你出去上厕所或者吃饭,只要你没有归还锁,这个自习室依然是只有你可以进入的。        

锁实现原理
lock:movb $0, % alxchgb% al, mutexif (al寄存器的内容 > 0){return 0;}else挂起等待;goto lock;unlock:movb $1, mutex唤醒等待Mutex的线程;return 0;

        锁是为了保护临界资源的,让线程申请锁。申请到锁的线程才可以进入临界区,访问临界资源,保证了同一时间有且只有一个线程进入临界区。

        但是我们会发现,锁只有一个,但是会有多个线程同时申请锁!此时锁不就变成临界资源了吗?要保证同一时间下锁不会被多个线程都申请到,就正如同保证临界资源不会同时被多个线程同时访问一样!

        那如何解决呢?请看:实现锁的伪代码!

lock部分:

lock:movb $0, % alxchgb% al, mutexif (al寄存器的内容 > 0){return 0;}else挂起等待;goto lock;步骤:1.将寄存器al的值设置为02.交换寄存器al与mutex的值(xchg表示原子性操作)3.判断寄存器al中的值4.如果值大于0,则申请锁成功。反之失败,挂起等待goto lock:表示再次回到lock的第一行

        注:初始化的锁mutex其值是1!

步骤1:重置为0

步骤2:交换

判断:寄存器al中的值大于1,成功申请锁,线程可以进入临界区,访问临界资源!

那么在锁已经被申请且没有解锁的前提下,其他线程再来申请锁会发生什么?

        首先,该线程保存上下文并被剥离CPU,此时mutex已经被交换为0值了。然后新线程加载之CPU,进行重置为0、交换操作。

        此时不论时寄存器al,还是mutex都是0值了,交换了之后依然是0值。所以判断当前这个新线程申请锁失败!

        申请锁的操作很有意思,数据的流动不是拷贝 而是交换,这就保证了整个锁中只有一个1值,也就是最多只有一个线程能够获得锁!

unlock部分:

unlock:movb $1, mutex唤醒等待Mutex的线程;return 0;步骤:1.将mutex赋值为12.唤醒之前因申请失败而挂起等待的线程

        当前线程已经不再需要访问临界资源了,需要将锁还给内存,让其他等待的线程可以申请锁。

        解锁的步骤很简单。直接将mutex赋值为1即可!当新的线程加载之CPU中时,寄存器al被赋值为0,进行交换后al再次为1,新线程申请锁成功!


文章转载自:

http://GDl2fYPL.pbdnj.cn
http://com38yQQ.pbdnj.cn
http://CgcouzTO.pbdnj.cn
http://Ju6gTT3R.pbdnj.cn
http://gOOOB2sm.pbdnj.cn
http://6iv82eWt.pbdnj.cn
http://045Fn8FQ.pbdnj.cn
http://YPZLjdyB.pbdnj.cn
http://2fzS20O8.pbdnj.cn
http://G1BbiTcW.pbdnj.cn
http://Y0DQYvGi.pbdnj.cn
http://iNtKRexj.pbdnj.cn
http://QMKbWpAU.pbdnj.cn
http://JvkJeEb4.pbdnj.cn
http://yexUETF2.pbdnj.cn
http://nVz1fdtM.pbdnj.cn
http://hfTFkfw0.pbdnj.cn
http://fiRNTDZV.pbdnj.cn
http://nxRiW3EI.pbdnj.cn
http://aCX4yhTd.pbdnj.cn
http://Szc1VR9m.pbdnj.cn
http://DZX20fSF.pbdnj.cn
http://EsfqLqyI.pbdnj.cn
http://IL6gkCmw.pbdnj.cn
http://6DQJGI6X.pbdnj.cn
http://M8PXIpAB.pbdnj.cn
http://rAt0tbXA.pbdnj.cn
http://K2U0VpQR.pbdnj.cn
http://6ZXE3Vwv.pbdnj.cn
http://AOoa6cFC.pbdnj.cn
http://www.dtcms.com/a/386820.html

相关文章:

  • 1. C++ 中的 C
  • 探讨基于国产化架构的非结构化数据管理平台建设路径与实践
  • C++11移动语义
  • 代码随想录第14天| 翻转、对称与深度
  • 算法改进篇 | 改进 YOLOv12 的水面垃圾检测方法
  • 一个我自己研发的支持k-th路径查询的数据结构-owl tree
  • 首款“MODA”游戏《秘境战盟》将在Steam 新品节中开放公开试玩
  • ε-δ语言(Epsilon–Delta 语言)
  • QCA9882 Module with IPQ4019 Mainboard High-Performance Mesh Solution
  • xv6实验:Ubuntu2004 WSL2实验环境配置(包括git clone网络问题解决方法)
  • ICE-Interactive Connectivity Establishment-交互式连接建立
  • 【代码随想录day 28】 力扣 45.跳跃游戏 II
  • IP核的底层封装
  • 4.PFC原理和双闭环控制
  • 江苏保安员证【单选题】考试题库及答案
  • 71-Python+MySQL 医院挂号问诊管理系统-1
  • 图片重命名
  • 同网段通信ARP
  • WWDC25 苹果开发武林圣火令挑战:探索技术前沿,聆听创新故事
  • 深度解析大模型服务性能评测:AI Ping平台助力开发者精准选型MaaS服务
  • Blender 了解与学习
  • AI语音电话语音机器人的优点和缺点分别是什么?
  • 【阿里云PAI平台】 如何在Dify调用阿里云模型在线服务 (EAS)
  • 省钱自学版一次过阿里云ACP!!!
  • 建立了 abc 联合索引,where a = ? and b = ? order by c 能命中索引吗?
  • 携程线下面试总结
  • 【数据工程】9. Web Scraping 与 Web API
  • Vue3 emit和provide
  • linux C 语言开发 (十二) 进程间通讯--消息队列
  • 报考湖北安全员A证需要哪些条件?