【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

news/2024/4/28 21:19:18/文章来源:https://blog.csdn.net/DEXTERFUTIAN/article/details/131719266

  • 引入
  • 互斥
    • 初识锁
    • 互斥量mutex
    • 锁原理解析
  • 可重入VS线程安全
    • STL中的容器是否是线程安全的?
  • 死锁

引入

我们写一个多线程同时访问一个全局变量的情况(抢票系统),看看会出什么bug:

// 共享资源, 火车票
int tickets = 10000;
//新线程执行方法
void *getTicket(void *args)
{std::string username = static_cast<const char *>(args);while (true){if (tickets > 0){usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒std::cout << username << " 正在进行抢票: " << tickets << std::endl;// 用这段时间来模拟真实的抢票要花费的时间tickets--;}else{break;}}return nullptr;
}
//主线程
//跟之前一样创建多个线程然后调用这个getTicket方法就行

假如创建4个线程同时抢票,总票数有10000张,每个线程抢到票以后减一,按照正常情况我们应该是抢票到0截至。

多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换

在这里插入图片描述
实验结果:假如此时tickets = 1,第1号线程先判断了if (tickets > 0)然后进入语句中,结果被usleep阻塞了,这时候切换了另外的线程,此时tickets的值还未被1号进程更改,所以它同样也能进入语句中,就这样导致tickets--被执行了多次,然后就出现上述的负数结果。

对变量进行++,或者–,在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:

  1. 从内存读取数据到CPU寄存器中
  2. 在寄存器中让CPU进行对应的算逻运算
  3. 写回新的结果到内存中变量的位置

模拟一下线程1与线程2的更改切换逻辑:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题!


互斥

初识锁

  • 临界资源:多个执行流进行安全访问的共享资源
  • 临界区:我们把多个执行流中,访问临界资源的代码(往往是线程代码的很小的一部分)
  • 互斥:想让多个线程串行访问共享资源,任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:对一个资源进行访问的时候,要么不做,要么做完,该操作只有两态,不会被任何调度机制打断的操作。(只用一条汇编语句就能执行完的算是原子,当然还有别的情况之后再详细说明)

在这里插入图片描述

在这里插入图片描述
初步使用加锁解锁:

//定义为全局的锁,无需初始化和销毁,直接以这样的形式使用:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *getTicket(void *args)
{std::string username = static_cast<const char *>(args);while (true){pthread_mutex_lock(&lock);if (tickets > 0){usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒std::cout << username << " 正在进行抢票: " << tickets << std::endl;tickets--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}

运行现象:
在这里插入图片描述
我们可以很明显感受到,运行的速度变慢了,而且这里的线程4一直在抢票,别的线程没有机会

  • 加锁和解锁的过程多个线程串行执行的,程序变慢了!
  • 锁只规定互斥访问,没有规定必须让谁优先执行
  • 锁就是真是的让多个执行流进行竞争的结果

线程解锁后,立马又申请锁,导致别的线程竞争不过,我们可以在每次循环末尾增加一段阻塞时间:
在这里插入图片描述

如何看待锁?

  1. 锁本身就是一个共享资源(我们一份代码要使用锁,前提是我们能看到这个锁,这个资源),全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
  2. pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!
  3. 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞!
  4. 谁持有锁,谁进入临界区!

对锁的思考:
在这里插入图片描述

  • 如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么?阻塞等待
  • 如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,线程1可不可以被切换呢?绝对可以的。当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行,直到我最终释放这个锁!

所以,对于其他线程而言,有意义的锁的状态,无非两种:1.申请锁前2.释放锁后。站在其他线程的角度看待当前线程持有锁的过程,就是原子的!

未来我们在使用锁的时候,一定要尽量保证临界区的粒度(锁中间保护代码的多少)要非常小!

有人可能会想,加锁也未必安全,比如我让线程12加锁去访问公共资源,线程3不加锁去访问公共资源,这样的话公共资源依旧没有被保护起来。加锁是程序员行为,必须做到要加就都要加


互斥量mutex

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

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

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

要解决以上问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
    要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
    在这里插入图片描述

初始化互斥量有两种方法:

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

销毁互斥量需要注意:

  • 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

互斥量加锁和解锁:

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

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

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

锁原理解析

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

在这里插入图片描述
加锁执行图:
在这里插入图片描述
在这里插入图片描述
接下来的代码就是判断,寄存器中的代码与数据是否符合。

在线程1执行后面的内容时,时刻都可能被切换,但是切换了线程2,寄存器的内容也会被切换掉,这样就算怎么执行,线程2寄存器中始终都是0。再切换回线程1,由上下文保护,寄存器内容切换回原来的数字1。
在这里插入图片描述
解锁:movb $1, mutex:就是将1重新给mutex

这个mutex原本的1就像是一个令牌,它有且只有一个,谁先抢到就能先运行


可重入VS线程安全

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

常见的线程不安全的情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入的情况:

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系:

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

可重入与线程安全区别:

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

STL中的容器是否是线程安全的?

不是安全的,原因是: STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。


死锁

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

感性认识死锁:小明与小红各自有5毛钱,它们一起去商店买棒棒糖,商店老板说它们的棒棒糖1块钱一个,小明对小红说能不能把她的5毛钱给他,这样他将能凑1块钱买棒棒糖,小红不乐意,她反问小明能不能把他的5毛钱给自己,这样她就能买棒棒糖,它们互相僵持,最后都没买到棒棒糖。他们之前出现的互相僵持的情况就是死锁

死锁四个必要条件:

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁方法:

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法:死锁检测算法、银行家算法


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

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

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

相关文章

用友畅捷通T+服务器数据库中了locked勒索病毒怎么办,如何处理解决

计算机技术的发展&#xff0c;也为网络安全埋下隐患&#xff0c;其中勒索病毒攻击已经成为企业和组织面临的严重威胁之一。作为一款被广泛使用的企业资源管理软件&#xff0c;用友畅捷通T系统也成为黑客攻击的目标之一。近期&#xff0c;我们收到很多企业的求助&#xff0c;公司…

Android Studio 的版本控制Git

Android Studio 的版本控制Git。 Git 是最流行的版本控制工具&#xff0c;本文介绍其在安卓开发环境Android Studio下的使用。 本文参考链接是&#xff1a;https://learntodroid.com/how-to-use-git-and-github-in-android-studio/ 一&#xff1a;Android Studio 中设置Git …

Intel RealSense D455(D400系列) Linux-ROS 安装配置(亲测可用)

硬件&#xff1a;Intel RealSense D455 系统&#xff1a;Ubuntu 18.04 Part_1: 安装librealsense SDK2.0 1.1 注册密钥 sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE或者 sudo apt-key adv --keyserver hkp:/…

【RabbitMQ】golang客户端教程1——HelloWorld

一、介绍 本教程假设RabbitMQ已安装并运行在本机上的标准端口&#xff08;5672&#xff09;。如果你使用不同的主机、端口或凭据&#xff0c;则需要调整连接设置。如果你未安装RabbitMQ&#xff0c;可以浏览我上一篇文章Linux系统服务器安装RabbitMQ RabbitMQ是一个消息代理&…

25.10 matlab里面的10中优化方法介绍—— 函数fmincon(matlab程序)

1.简述 关于非线性规划 非线性规划问题是指目标函数或者约束条件中包含非线性函数的规划问题。 前面我们学到的线性规划更多的是理想状况或者说只有在习题中&#xff0c;为了便于我们理解&#xff0c;引导我们进入规划模型的一种情况。相比之下&#xff0c;非线性规划会更加贴近…

联想北京公司研发管理部高级经理周燕龙受邀为第十二届中国PMO大会演讲嘉宾

联想&#xff08;北京&#xff09;有限公司研发管理部高级经理周燕龙先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;PMO如何助力研发。大会将于8月12-13日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&#xff1a; PMO在…

gitee使用参考

Git代码托管服务 2.1 常用的Git代码托管服务 gitHub&#xff08; 地址&#xff1a;https://github.com/ &#xff09;是一个面向开源及私有软件项目的托管平台&#xff0c;因为只支持Git 作为唯一的版本库格式进行托管&#xff0c;故名gitHub码云&#xff08;地址&#xff1a;…

Docker安装部署ShardingProxy详细教程

&#x1f680; ShardingSphere &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&…

Go 下载安装教程

1. 下载地址&#xff1a;The Go Programming Language (google.cn) 2. 下载安装包 3. 安装 &#xff08;1&#xff09;下一步 &#xff08;2&#xff09;同意 &#xff08;3&#xff09;修改安装路径&#xff0c;如果不修改&#xff0c;直接下一步 更改后&#xff0c;点击下一…

13个ChatGPT类实用AI工具汇总

在ChatGPT爆火后&#xff0c;各种工具如同雨后春笋一般层出不穷。以下汇总了13种ChatGPT类实用工具&#xff0c;可以帮助学习、教学和科研。 01 / ChatGPT for google/ 一个浏览器插件&#xff0c;可搭配现有的搜索引擎来使用 最大化搜索效率&#xff0c;对搜索体验的提升相…

DataStructure--Basic

程序设计数据结构算法 只谈数据结构不谈算法就跟去话剧院看梁山伯与祝英台结果只有梁山伯在演&#xff0c;祝英台生病了没来一样。 本文的所有内容都出自《大话数据结构》这本书中的代码实现部分&#xff0c;建议看书&#xff0c;书中比我本文写的全。 数据结构&#xff0c;直…

2023.07.13力扣6题

931. 下降路径最小和 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径可以从第一行中的任何元素开始&#xff0c;并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列&#xff08;即位…

【数据结构】无头+单向+非循环链表(SList)(增、删、查、改)详解

一、链表的概念及结构 1、链表的概念 之前学习的顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;而链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的&#xff0c;可以实现更加…

SpringBoot项目连接数据库

1、找到applications.yml&#xff0c;如下图 2、写入代码 server:port: 9494spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/自己的数据库表名?serverTimezoneGMT%2b8username: rootpassword: root

[C语言] 数组

1. 一维数组的创建和初始化 2. 一维数组的使用 3. 一维数组在内存中的存储 4. 二维数组的创建和初始化 5. 二维数组的使用 6. 二维数组在内存中的存储 7. 数组越界 8. 数组作为函数参数 9. 数组的应用实例 1 &#xff1a;三子棋 10. 数组的应用实例 2 &#…

Spring Tool Suite 4

参考&#xff1a;Spring tool suite4 安装及配置_springtoolsuite4_猿界零零七的博客-CSDN博客 下载&#xff1a;Spring | Tools 将下载的JAR进行解压两次&#xff0c;直至解压出contents中的sts 双击启动 第一次打开需要指定工作区文件夹 配置Maven的config 安装插件

Pytorch学习笔记1:张量+训练参数传入与处理+制作训练集

文章目录 Pytorch中张量的一些常见函数最基础也最常见的方法关于Indexing, Slicing, Joining, Mutating Ops&#xff08;索引、切片、聚合、旋转&#xff09;随机种子torch.bernoulli(input)torch.normaltorch.rand(size)torch.randn(size)torch.randperm(n) Python--argparse-…

Hexo+GithubPages免费搭建个人博客网站

HexoGithubPages免费搭建个人博客网站 目录 一、前言二、Github配置 新建同名仓库配置Pages 三、安装Hexo四、配置hexo-deployer-git五、访问六、发布文章七、安装主题 一、前言 我之前开了好几年的云服务器了&#xff0c;实际上使用场景并不是很多&#xff0c;感觉有点浪费…

什么叫前后端分离?为什么需要前后端问题?解决了什么问题?

单体架构出现的问题 引出&#xff1a;来看一个单体项目架构的结构 通过上述可以看到单体架构主要存在以下几点问题&#xff1a; 开发人员同时负责前端和后端代码开发&#xff0c;分工不明确开发效率低前后端代码混合在一个工程中&#xff0c;不便于管理对开发人员要求高(既会前…

网络层中一些零碎且易忘的知识点

异构网络&#xff1a;指传输介质、数据编码方式、链路控制协议以及数据单元格式和转发机制不同&#xff0c;异构即物理层和数据链路层均不同RIP、OSPF、BGP分别是哪一层的协议&#xff1a; -RIPOSPFBGP所属层次应用层网络层应用层封装在什么协议中UDPIPTCP 一个主机可以有多个I…