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

Linux:多线程---深入互斥浅谈同步

文章目录

      • 1. 互斥
        • 1.1 为什么需要互斥
        • 1.2 互斥锁
        • 1.3 初谈互斥与同步
        • 1.4 锁的原理
        • 1.5 可重入VS线程安全
        • 1.6 死锁
        • 1.7 避免死锁的算法(扩展)

  • 序:在上一章中我们知道了线程控制的三个角度:线程创建、线程等待和线程终止,分别从接口以及参数的意义和功能的角度来了解,以及最后深入原生线程库,了解用户级线程与内核轻量型进程的关系。而本章将从线程的同步与互斥的角度来带大家了解什么是互斥和同步,以及为什么要互斥和同步等一系列问题。

上一章线程控制的知识补充:

线程分离:
在这里插入图片描述
pthread_detach函数,可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,分离后的线程不可被等待,如果强行等待也会返回错误码22。

问题一:为什么要有线程分离呢?

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

归根结底,我们让线程分离,其实就是更改线程的原生线程库里的tcb内的分离的属性,而pthread_join就是识别到了该分离属性被更改为已分离,所以才会直接返回一个错误码。

1. 互斥

1.1 为什么需要互斥

多线程抢票模型代码演示:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>
using namespace std;#define NUM 4int ticket =100;//用多线程,模拟一轮抢票class ThreadData
{
public:ThreadData(int number){_thread_name ="thread-" + to_string(number);}
public:string _thread_name;
};void* GetTicket(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);const char* name =td->_thread_name.c_str();while(true){if(ticket>0){usleep(5000);printf("i am %s,get a ticket:%d\n",name,ticket);ticket--;}else break;}printf("%s ... quit\n",name);return nullptr;}
int main()
{vector<pthread_t> tids;vector<ThreadData*> thread_datas;for(int i=0;i<NUM;i++){pthread_t tid;ThreadData* td=new ThreadData(i);thread_datas.push_back(td);pthread_create(&tid,nullptr,GetTicket,thread_datas[i]);tids.push_back(tid);}for(auto &e :tids){pthread_join(e,nullptr);}for(auto &e :thread_datas){delete e;}return 0;
}

结果如图:

在这里插入图片描述
那么问题来了抢票模型中,为什么抢票抢到最后,竟然抢到了负数?这个问题我们暂且不谈,我们继续往下说。

要想了解为什么会出现这样的情况,我们首先就要知道既然ticket出现了负数,就说明ticket–出现了问题,共享数据------>数据不一致问题!!!(肯定和多线程并发访问是有关系的),对一个全局变量进行多线程并发–或++操作是否是安全的?所以这个–操作不是原子的,所以也不是安全的。

既然–操作是不安全的,不是原子的,那我们要了解ticket–究竟要有哪些步骤:
在这里插入图片描述

第一个步骤:先将ticket读入到CPU的寄存器当中
第二个步骤:CPU内部进行–操作
第三个步骤:将计算结果写回内存

但是想要了解为什么会出现这样的情况,我们还要了解一个额外的知识点:寄存器不等于寄存器的内容线程在执行的时候,将共享数据,加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文 — 这样的数据以拷贝的形式给自己单独拿了一份

既然ticket–是有三个步骤组成,如果在这三个步骤之内发生了线程切换就会导致数据不一致的问题!!!

在这里插入图片描述
假设当前抢票的进程中,票有1000张,该进程内有两个线程正在抢票,此时thread-1线程正在实现抢票,刚完成第一步,将内存中的数据读入寄存器中,也就是读入该线程的上下文中,如果此时来了第二个线程thread-2也要实行抢票,将thread-1线程切换了,thread-1线程就会带着这个1000的数据一起离开,等待线程再次切换回来!!!
在这里插入图片描述
当线程切换到thread-2线程,假设此时thread-2线程在下一次线程切换的时间片内进行了100次抢票的动作,此时的票数就由1000变成了900
在这里插入图片描述

当thread-2线程抢了100张票后,将寄存中的900写回给内存中的ticket,此时thread-1线程切换回来了,thread-2线程就要带着自己的硬件上下文走,于是thread-2就将寄存器中的900带走了,thread-1线程切换回来后,会将之前就带走的寄存器中的1000带回来,在放入到寄存器中,即恢复上下文。
在这里插入图片描述
然后,此时thread-1会继续执行未完成的动作,继续执行第二步和第三步。
在这里插入图片描述
这就会导致,thread-2辛辛苦苦抢的票,将1000变成900,结果thread-1线程切换回来后就变成了999。

现在让我们来回答最开始的那个问题,为什么ticket会出现负数?
在这里插入图片描述
假设此时的实际票数小于线程数,此时有四个线程,但票只有两个了,别忘了ticket>0也是运算(逻辑运算)所以此时同时有4个线程都对ticket进行了逻辑运算,此时票有两个,都是大于0,此时四个线程都进入了该循环内,都可以进行ticket–,这也就是为什么会出现了负数!!!

1.2 互斥锁

怎么解决上述的一些列问题???
对共享数据的任何访问,保证任何时候只有一个执行流进行访问!—互斥!!!
而想要实现互斥就要引入互斥锁的概念!!!

在这里插入图片描述

锁资源的定义,初始化和释放:

pthread_mutex_t是库提供的一种数据类型
pthread_mutex_init(第一个参数传锁,第二个参数传锁的各种参数(默认传nullptr))
pthread_mutex_destroy
(如果用pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALZER进行全局变量的初始化,就不用调用pthread_mutex_destroy函数进行释放)

一种临界资源,由多个线程访问,如果想要保证临界资源的安全,就必须让这个多个线程访问同一把锁!!!

在这里插入图片描述

锁的申请和释放:

pthread_mutex_lock
pthread_mutex_unlock
其中的pthread_mutex_trylock函数就是加锁的非阻塞版本

到这一步,大家只能对锁有个印象,没法深刻知道锁的作用和锁的使用,接下来我将改进原本的多线程抢票模型的代码,用互斥锁来使原版中的多线程导致的数据不一致问题得到解决!!!

互斥锁版的多线程抢票模型代码演示

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>using namespace std;#define NUM 5int ticket =100;//用多线程,模拟一轮抢票class ThreadData
{
public:ThreadData(int number,pthread_mutex_t* lock){_thread_name ="thread-" + to_string(number);_lock=lock;}
public:string _thread_name;pthread_mutex_t *_lock;
};void* GetTicket(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);const char* name =td->_thread_name.c_str();while(true){pthread_mutex_lock(td->_lock);if(ticket > 0){//usleep(5000);printf("i am %s,get a ticket:%d\n",name,ticket);ticket--;}else{pthread_mutex_unlock(td->_lock);break;}pthread_mutex_unlock(td->_lock);usleep(5000);}printf("%s ... quit\n",name);return nullptr;
}int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);vector<pthread_t> tids;vector<ThreadData*> thread_datas;for(int i=0;i<NUM;i++){pthread_t tid;ThreadData* td=new ThreadData(i,&lock);thread_datas.push_back(td);pthread_create(&tid,nullptr,GetTicket,thread_datas[i]);tids.push_back(tid);}for(auto &e :tids){pthread_join(e,nullptr);}for(auto &e :thread_datas){delete e;}pthread_mutex_destroy(&lock);return 0;
}

对于上面一段代码,我们用锁将临界资源锁住,同一时间只能有一个线程进行访问,从而实现对临界资源的保护

其中,被锁保护的资源叫做临界资源,某几段访问临界资源的代码区叫做临界区

加锁的本质:是用时间来换安全
加锁的表现:线程对临界区代码的串行执行
加锁原则:尽量保证临界区代码越少越好。
申请锁成功了,才能往后执行,不成功,就会阻塞等待(等待锁资源释放)

不同线程对于锁的竞争能力可能会不同,在纯互斥环境中,如果锁分配不够合理,容易导致其他线程的饥饿问题----->不是说只要有互斥就必有饥饿,适合纯互斥场景就用互斥。

1.3 初谈互斥与同步

目前,我们对于锁的概念已经有了一个清晰的认识了,但是我们发现了一个新的问题,当一个线程申请锁,完成对临界资源的访问后,释放锁后,该线程可能也会申请锁,这就可能出现一个线程一直在申请和释放锁,导致其他线程没办法申请到锁,对于这种情况,就要深入了解同步的概念来解决新出现的问题,现在让我们更加深入的了解互斥与同步吧

现在有一个vip自习室:
在这里插入图片描述
vip自习室规定:1. 外面的同学想要进入vip自习室必须排队。2. 出来的同学,将钥匙放好后,不能立马重新拿钥匙进vip自习室,如果想要再次进入自习室则必须排到队列的尾部进行排队

vip自习室的规则让所有的同学(线程)按照一定的顺序拿到钥匙(锁)进入vip自习室,而按照一定的顺序性获取资源的模式就是同步!!!

1.4 锁的原理

锁本身也是一种临界资源!!!所以。申请锁和释放锁本身就被设计成为了原子性操作了(问题:如何做到的???)

在临界区中,线程可以被切换吗?可以切换!!!在线程被切出去的时候,是持有锁被切走的。该线程即使被切换走了,照样没有任何线程能进入资源临界区访问临界资源!

对于其他线程来讲,一个线程要么没有锁,要么释放锁。当前线程访问临界区的过程,对于其他线程就是原子的!!!

问题一:为什么说ticket–不是原子的?因为该语句会变成多条汇编语句,在该汇编语句的中间,如果有其他线程也在执行,就会出错,出现不一致的情况,换言之,只要汇编语句只有一条就没有问题,所以,原子:只有一条汇编语句就是原子的!!!

lock的汇编语句:xchqb %al,mutex
Xchqb交换的本质:把内存中的数据(共享),交换到CPU的寄存器中(把数据交换到线程的硬件的上下文中)—>把一个共享的锁,让一个线程以一条汇编语句的方式,交换到自己的上下文中!!!(这就叫做当前线程持有锁了!!!)

unlock的汇编语句:movb $1,mutex:将一个共享的锁从线程的上下文中拿出来。(这就叫做当前线程释放锁了!!!)

1.5 可重入VS线程安全

线程安全的概念:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有有锁保护的情况下,会出现该问题。
重入的概念:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数、不则具不可重入函数。

可重入与线程安全联系:

函数是可重入的,那就是线程安全的。
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别:

可重入函数是线程安全函数的一种。
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

1.6 死锁

死锁的概念:

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁产生的必要条件:

互斥条件:一个资源每次只能被一个执行流使用(前提)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(原则)
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺(原则)
死锁产生的充分条件:
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系(重要条件)
这三个必要条件必须同时满足才是死锁。

如何避免死锁问题?

1. 破坏死锁的四个必要条件,只需要一个不满足就可以了
2. 加锁顺序一致
3. 避免锁未释放的场景
4. 资源一次性分配

1.7 避免死锁的算法(扩展)

银行家算法:

下面是银行家算法的模拟实现,感兴趣的小伙伴可以去了解

银行家算法避免死锁

总结:

本篇博客先是补充了上一章中对于线程分离的知识缺失的内容补全,而后,从一小段代码出发,在多线程的抢票模型下,我们逐步发现多线程带来的问题,并逐步解决,为了解决这些问题,我们先后引入了互斥和同步的概念,最后有队线程安全问题和可重入的问题进行了了解,并讲述了死锁的概念及其产生的条件,最后以避免死锁的银行家算法结尾,谢谢大家的支持!!!

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

相关文章:

  • vue中添加原生右键菜单
  • LucidShape 2024.09 最新
  • FreeCAD傻瓜教程-拉簧拉力弹簧的画法及草图的附着位置设定和Part工作台中形体构建器的妙用
  • Flutter 使用http库获取网络数据的方法(一)
  • 初识Linux:Linux开发工具gcc/g++和gdb以及Makefile的使用
  • App爬虫工具篇-appium配置
  • 【STM32实践篇】:GPIO 详解
  • 2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--上篇
  • 九、K8s污点和容忍
  • web前端面试-- MVC、MVP、MVVM 架构模式对比
  • 递归与循环
  • 高频交易服务器篇
  • A/B测试实战:页面微小改动如何带来30%转化率提升?
  • ABC413 : E Reverse 2^i
  • Vue前端项目接收webSocket信息
  • Linux网络配置与故障排除完全指南
  • 介绍electron
  • 【ES6】Latex总结笔记生成器(网页版)
  • TailWind CSS Intellisense 插件在VSCode 上不生效
  • LESS/SCSS 高效主题换肤方案
  • 基于 LangChain 实现通义千问 + Tavily 搜索 Agent 的简单实践
  • 在VMware虚拟机中安装Windows 98时,Explorer提示“该程序执行了非法操作,即将关闭”的解决办法
  • 虚拟机与容器技术详解:VM、LXC、LXD与Docker
  • php协程
  • MySQL 数据库传统方式部署主从架构的实现很详细
  • React Native 亲切的组件们(函数式组件/class组件)和陌生的样式
  • 若 VSCode 添加到文件夹内右键菜单中显示(通过reg文件方式)
  • 盘式制动器的设计+说明书和CAD)【6张】+绛重
  • Redis性能优化
  • 权电阻网络DAC实现电压输出型数模转换Multisim电路仿真——硬件工程师笔记