linux:线程互斥

在这里插入图片描述

个人主页 : 个人主页
个人专栏 : 《数据结构》 《C语言》《C++》《Linux》

文章目录

  • 前言
  • 一、线程互斥
    • 问题
    • 解释
    • 互斥量的接口
  • 二、加锁的原理
  • 三、 死锁
      • 死锁四个必要条件
      • 避免死锁
  • 总结


前言

本文是对于线程互斥的知识总结


一、线程互斥

问题

我们先看下面代码。

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>const int numbers = 3;int ticket = 10000;
void *threadRoutine(void *args)
{std::string name = static_cast<char *>(args);while (true){if (ticket > 0){usleep(1000);std::cout << name << "get a ticket: " << ticket << std::endl;ticket--;}else{break;}}return nullptr;
}int main()
{std::vector<pthread_t> tds;for (int i = 0; i < numbers; ++i){pthread_t td;char buff[64];snprintf(buff, sizeof(buff), "thread-%d", i);pthread_create(&td, nullptr, threadRoutine, (void *)buff);usleep(1000);tds.push_back(td);}pthread_join(tds[0], nullptr);pthread_join(tds[1], nullptr);pthread_join(tds[2], nullptr);return 0;
}

该代码创建三个线程-1,线程-2,线程-3,去抢夺ticket资源,当ticket从10000依次减到0时,三个线程退出。那该代码运行结果是什么呢?ticket最后会是0吗?
在这里插入图片描述
显而易见ticket最后是-1!这是为什么?三个线程在ticket为0时,不应该退出吗,ticket为什么会是-1?更奇怪的还是下图
在这里插入图片描述
ticket值尽然有相同的情况,ticket的值不应该依次递减吗?


解释

先不要着急,我们先来明确几个概念

  • 临界资源:多线程执行流共享的资源叫做临界资源(如上面代码中的ticket全局变量)

  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区(如下图红框部分的代码)

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

  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

在了解上面四个概念后,我们还需要了解,在C++/C中前置++ or 后置++,判断操作是原子的吗?
在这里插入图片描述
在这里插入图片描述

显然这些操作都不是原子的,判断在汇编指令是要先判断,再依据判断结果跳转执行流,后置–是先从内存中读取变量的值放到寄存器中,再对寄存器中的值-1, 最后将寄存器中的值放回变量中。

现在知道了上述知识,我们可以来理解为什么ticket会不变和变为-1。

我们现将这些汇编指令分别表明为步骤1,步骤2…
在这里插入图片描述
我们先来解释为什么ticket的值可能不变。
在这里插入图片描述
当线程thread-0执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-0被线程thread-1切换,寄存器中的34,会作为线程thread-0的上下文数据,被线程thread-0保留,此时线程thread-1执行步骤3,将内存中icket的值34,读取到寄存器中;线程thread-1被线程thread-2切换,同理寄存器中的34作为线程thread-1的上下文数据被保留;此时线程thread-2执行步骤3,将内存中icket的值34,读取到寄存器中,再执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33放回到内存中ticket处,此时ticket = 33;线程thread-2打印ticket的值;线程thread-2被线程thread-0切换,要恢复线程thread-0的上下文数据,寄存器中存储的是34,线程thread-0在执行步骤4,将寄存器中的34变为33,在执行步骤5,将寄存器中的33拷贝到内存中ticket处,ticket = 33,线程thread-0打印ticket的值为33;同理线程thread-2最后也会打印ticket的值为33。这就是线程0,1,2都会打印33的原因。
在这里插入图片描述

明白了为什么ticket会打印3次33。那ticket为什么会变为-1,就好理解了。
我们假定此时ticket = 1;线程thread-0执行步骤1( 1 > 0)判断为真,被线程thread-1切换,判断结果作为线程thread-0的上下文数据被保存;线程thread-1也指向步骤1( 1 > 0)判断为真,在执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 1),再在寄存器内将1 -> 0,再将0拷贝到内存ticket处(ticket= 0),线程tithread-1被线程thread-2切换,线程thread-2执行步骤1( 0 > 0)判断为假,结束循环;线程thread-2被线程thread-0切换,线程thread-0执行步骤2,步骤3,步骤4,步骤5,从内存中读取ticket的值(ticket = 0),再在寄存器内将0 -> -1,再将-1拷贝到内存中ticket处(ticket = -1)。这就是ticket为什么会是-1的原因。

这就是我们多线程访问共享数据而导致数据不一致问题,那如何解决呢?

要解决以上数据不一致问题,就要保证只能有一个执行流在临界区执行代码。而这就是锁,linux上提供的这把锁叫做互斥量。
在这里插入图片描述


互斥量的接口

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

  • 静态分配
    在这里插入图片描述

  • 动态分配
    在这里插入图片描述
    mutex:要初始化的互斥量;
    attr:用于设置互斥锁的属性(传递 NULL 作为 attr 参数的值,那么互斥锁会使用默认的属性进行初始化。)

销毁互斥量
在这里插入图片描述
如果成功销毁互斥锁,则返回0;
如果发生错误,则返回错误码(EBUSY:在尝试销毁一个正在使用的互斥锁时,通常会返回这个错误;EINVAL:传递个函数的mutex指针无效)
需要注意的是:

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

互斥量加锁和解锁

在这里插入图片描述
加锁成功,返回0;加锁失败,返回错误码(如EDEADLK, EINVAL, EBUSY)
当互斥锁已经被其它线程锁定时,调用pthread_mutex_lock的线程通常会被阻塞,直到互斥锁被解锁。如果不想线程申请锁失败被阻塞,可以使用pthread_mutex_trylock函数。

在这里插入图片描述
加锁成功,返回0; 加锁失败,返回错误码(如EBUSY:当前互斥锁被其他线程锁定)
pthread_mutex_trylock不会让调用线程在互斥锁不可用时进入阻塞状态,这使得可以用轮询的方式来申请锁。

在这里插入图片描述
成功解除锁,返回0;解除锁失败,返回错误码(如传递给函数的mutex指针无效,解除一个未由当前线程锁定的锁)

现在我们对开始的代码进行改进

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>const int numbers = 3;
// 定义锁
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;int ticket = 10000;
void *threadRoutine(void *args)
{std::string name = static_cast<char *>(args);while (true){//加锁pthread_mutex_lock(&mutex);if (ticket > 0){usleep(1000);std::cout << name << "get a ticket: " << ticket << std::endl;ticket--;// 解锁pthread_mutex_unlock(&mutex);}else{// 解锁pthread_mutex_unlock(&mutex);break;}}
}int main()
{std::vector<pthread_t> tds;for (int i = 0; i < numbers; ++i){pthread_t td;char buff[64];snprintf(buff, sizeof(buff), "thread-%d", i);pthread_create(&td, nullptr, threadRoutine, (void *)buff);usleep(1000);tds.push_back(td);}pthread_join(tds[0], nullptr);pthread_join(tds[1], nullptr);pthread_join(tds[2], nullptr);return 0;
}

在这里插入图片描述
现在ticket == 1时,程序退出。

二、加锁的原理

pthread_mutex_t的结构如下:
在这里插入图片描述

我们先简单的将mutex这个结构体理解为一个int,将mutex = 1视为锁资源空闲,将mutex = 0视为锁资源已经被占用。
在这里插入图片描述
当一个线程要加锁时,其要先执行movb指令,将0移动到%al寄存器中(表示未持有锁),再执行xchgb指令,将mutex中的值与%al寄存器交换,如果%al寄存器中的值大于0,表示加锁成功,可以执行临界区代码;%al寄存器的值小于等于0,表示加锁失败,要挂起等待其它持有该锁的线程释放锁,再执行goto语句,重新申请锁。
在这里插入图片描述
当一个线程释放锁时,其要执行movb指令,将1移动到mutex处(表示锁资源空闲),再唤醒挂起等待的线程;

看了上面内容,我们可能还有点疑惑,为什么pthread_mutex_lock函数就是原子的呢?下面让我们已两个线程1,2申请锁为列,来理解。
我们假定当前有一个空闲的锁mutex,线程1执行movb指令,将0移动到%al寄存器中,线程1被线程2切换,线程1保存%al寄存器中的内容0;线程2执行movb指令,将0移动到%al寄存器中,再执行xchgb指令,将mutex的值与%al寄存器中的值交换(mutex = 0, %al = 1),线程2被线程1切换,%al寄存器中的内容1作为线程2的上下文数据被保存,线程1回复上下文数据(%al = 0),再执行xchgb指令,交换mutex和%al寄存器的内容(%al = 0, mutex = 0),再执行if判断为假,线程1挂起等待;线程1被线程2切换,回复上下文数据(%al = 1),线程2执行if判断为真,线程2执行临界区代码。

在这里插入图片描述

现在我们就可以理解,为什么pthread_mutex_lock函数是原子的了。

三、 死锁

死锁是指在一组进程中各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
如下图所示:
在这里插入图片描述
线程A,B只有同时拥有锁1,锁2才能访问临界区代码,此时线程A拥有lock1,申请lock2,线程B拥有lock2,申请lock1,线程A,B都会因为所申请的资源被其它线程所占有而等待,这就是死锁。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获取资源保持不放
  • 不剥夺条件:一个执行流已获取的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间现成一个头尾相接的循环等待资源的关系(如上图,线程A需要线程B持有的lock2,线程B需要线程A持有的lock1,线程A,B形成头尾相接的循环等待)

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致(如要求线程A,B都因先申请lock1,再申请lock2)
  • 避免锁未释放的场景(如将pthread_mutex_unlock误写成pthread_mutex_lock,从而导致只有一个线程造成的死锁)
  • 资源一次性分配

总结

以上就是我对于线程互斥的总结。

在这里插入图片描述

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

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

相关文章

无与伦比的技术平台

侧记订单 欢迎评论关注点赞收藏并转发支持&#xff0c;本人将不胜惶恐。本文由蜗牛老师开发Butterfly一键发布工具发布

财报解读:出海“窗口期”再现,汇量科技保驾护航的底气源于什么

大数据时代&#xff0c;每个人的喜好都被精准捕捉。购物APP、购物网站们&#xff0c;都仿佛一位贴心的时尚顾问。而这源于个性化广告经过深度学习和智能算法得来的结果。 随着广告市场的竞争愈演愈烈&#xff0c;广告主们需要更为精准、高效的个性化投放。近日&#xff0c;深耕…

基于SSM的宿舍管理系统的设计与实现(JSP,MySQL)

摘 要 随着社会发展、信息技术的普及&#xff0c;人们日常管理工作也发生了巨大的变化。信息化技术之渗透各行业的方方面面。学生宿舍管理作为校园管理工作的重要一环&#xff0c;不仅关系到学生自身的确切利益&#xff0c;同时也是对校园管理工作重大考验。近来年由于在校学生…

rtt自动初始化机制学习

通过以下两篇文章基本能搞懂rtt的自动初始化机制&#xff0c;从此你也可以借鉴写自己的自动初始化段(section)。 1点这里 https://blog.csdn.net/qq_38824401/article/details/119717389 2点这里 https://club.rt-thread.org/ask/article/d686458bbba864f4.html section背景…

leetcode代码记录(移除链表元素

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head […

Flutter开发进阶之瞧瞧Widget

Flutter开发进阶之瞧瞧Widget 在Flutter开发中,WIdget是构建界面的基本单元;Widget是不可变的,意味着一旦创建如果需要改变UI就需要重新创建一个新的Widget;在实际开发中,Widget通常由一个个Widget组合而成,从而形成嵌套的树形结构,复杂的UI就是由这一个个Widget构建而…

【C语言】—— 指针三 : 参透数组传参的本质

【C语言】—— 指针三 &#xff1a; 参透数组传参的本质 一、数组名的理解二、使用指针访问数组2.1、指针访问数组2.2、[ ] 的深入理解2.3、数组与指针的区别 三、一维数组的传参本质四、数组指针变量4.1、数组指针变量是什么4.2、 数组指针的初始化 五、二维数组传参的本质 一…

简单了解多线程

并发和并行 并发&#xff1a; 在同一时刻&#xff0c;多个指令在单一CPU上交替指向 并行&#xff1a;在同一时刻&#xff0c;多个指令在多个CPU上同时执行 2核4线程&#xff0c;4核8线程&#xff0c;8核16线程&#xff0c;16核32线程 基础实现线程的方式 Thread :继承类 &…

多人命题系统|基于SSM框架+ Mysql+Java+ B/S结构的多人命题系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

Python从入门到精通秘籍九

一、Python中文件编码概念 在Python中&#xff0c;文件编码指的是将文本内容转换为字节序列的过程。不同的编码方式使用不同的字符集和字节表示形式。下面是一个示例代码&#xff1a; # 写入文本到文件 text "你好&#xff0c;世界&#xff01;" with open("…

遥感图像地物分类流程

遥感图像地物分类流程 1. 制作标签 使用arcgis pro或者arcgis或者envi&#xff0c;画标签&#xff0c;保存为tiff格式 2. 处理标签数据 用python gdal库安装 osgdal库&#xff0c;如果安装失败就需要下载 对应库得 .whl去安装&#xff0c;网站具体搞忘了&#xff0c;可以百…

13年资深测试,性能测试常见指标分析总结,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、什么是性能测试…

Reactive: Vue3数据更新但是表单没有更新

这两天在折腾前端&#xff0c; 因为前端基础不牢靠&#xff0c; 所以&#xff0c;通常都是猜一半&#xff0c; 查一半&#xff0c; 基本上也就是个面向百度编程。 过程中碰到了一个问题&#xff0c; 就是在编辑table的时候&#xff0c; 明明我把pinia的数据缓存更新了&#xf…

LeetCode 面试经典150题 238.除自身以外数组的乘积

题目&#xff1a; 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c…

Kotlin runBlocking CoroutineScope synchronized简单死锁场景

Kotlin runBlocking CoroutineScope synchronized简单死锁场景 import kotlinx.coroutines.*fun main(args: Array<String>) {runBlocking {val lock1 Any()val lock2 Any()CoroutineScope(Dispatchers.IO).launch {repeat(10) {println("A-$it 申请 lock1...&quo…

【SpringBoot】优雅实现超大文件上传

前言 文件上传是一个老生常谈的话题了&#xff0c;在文件相对比较小的情况下&#xff0c;可以直接把文件转化为字节流上传到服务器&#xff0c;但在文件比较大的情况下&#xff0c;用普通的方式进行上传&#xff0c;这可不是一个好的办法&#xff0c;毕竟很少有人会忍受&#…

Http 超文本传输协议基本概念学习摘录

目录 HTTP协议 超文本传输协议 HyperText超文本 HTML超文本标记语言 HTTP协议原理 请求发送 服务器处理 响应发送 连接关闭或保持 HTTP协议版本 HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3 HTTP请求方法 GET POST PUT DELETE HEAD OPTIONS HTTP请求头字…

python中的类与对象

前言 在Python中&#xff0c;类是一种用于创建新类型对象的结构&#xff0c;它允许我们将数据和功能&#xff08;属性和方法&#xff09;封装到一个单独的逻辑单元中。类可以被看作是创建对象&#xff08;实例&#xff09;的蓝图或模板。类&#xff08;Class&#xff09;和对象…

JVM学习-类加载

目录 1.类文件结构 2.类加载器 3.类加载的三个阶段 3.1加载 3.2链接 3.2.1验证 3.2.2准备阶段 3.2.3解析阶段 3.3初始化 4.拓展&#xff1a;反射 4.1获取类对象 4.2创建实例 4.3获取方法 4.4方法调用 1.类文件结构 2.类加载器 类加载器用来将类文件的二进制字节码加载到JV…

猜数字游戏有三变(Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…