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

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

相关文章

基础乱炖来吧

1,SSH框架和SSM区别 SSH&#xff1a;structspringhibernate&#xff0c;SSM&#xff1a;MVCspringmybatis struct入口是filter级别&#xff0c;对action类进行请求&#xff0c;一个action类对应一个请求、类拦截&#xff1b;spring-mvcservlet级别&#xff0c;方法级别请求&…

海外代理IP在跨境电商中的五大应用场景

在我国跨境电商的发展中&#xff0c;海外代理IP的应用日益广泛&#xff0c;它不仅帮助商家成功打入国际市场&#xff0c;还为他们在多变的全球电商竞争中保持优势。下面是海外代理IP在跨境电商中五个关键的应用场景。 1、精准的市场分析 了解目标市场的消费者行为、产品趋势以…

Java语言: JVM

1.1 内存管理 1.1.1 JVM内存区域 编号 名字 功能 备注 1 堆 主要用于存放新创建的对象 (所有对象都在这里分配内存) jdk1.8之后永久代被替换成为了元空间&#xff08;Metaspace&#xff09; 2 方法区(加、常、静、即) 被虚拟机加载的类信息(版本、字段、方法、接口…

汽车电子零部件(8):T_Box

前言: 网联汽车(Connected Vehicles ,CV)是一个广泛的概念,四个主要的CV线程已发展起来:互联、自主、共享和电动。这些应用于包括CV在内的垂直领域:汽车、通信、互联网和共享手机服务。中国汽车工程师学会(SAEC)提倡将车载ADAS(高级驾驶员辅助系统)与通信技术相结合…

(挖矿病毒清除)kdevtmpfsi 处理

Linux Centos 7 环境下的一台服务器CPU直接被打满&#xff0c;上服务器 top 命令看到了一个未知的 kdevtmpfsi 疯狂占用中&#xff0c;情况如下图&#xff1a; 同时阿里云检测平台&#xff0c;也同步提示对应容器出现的问题 问题原因&#xff1a; postgres RCE导致h2Miner蠕虫…

ASP.NET Core 8.0 WebApi 从零开始学习JWT登录认证

文章目录 前言相关链接Nuget选择知识补充JWT不是加密算法可逆加密和不可逆加密 普通Jwt&#xff08;不推荐&#xff09;项目环境Nuget 最小JWT测试在WebApi中简单使用简单使用运行结果 WebApi 授权&#xff0c;博客太老了&#xff0c;尝试失败 WebApi .net core 8.0 最新版Jwt …

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:ListItemGroup)

该组件用来展示列表item分组&#xff0c;宽度默认充满List组件&#xff0c;必须配合List组件来使用。 说明&#xff1a; 该组件从API Version 9开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。该组件的父组件只能是List。 使用说明 当List…

51单片机LED8*8点阵显示坤坤跳舞打篮球画面

我们作为一名合格的 ikun&#xff0c;专业的小黑子&#xff0c;这个重要的知识必须学会。 先看效果&#xff1a; 51LED点阵_鸡你太美 这里我们首先要用到延时函数Delay&#xff1a; void Delay(unsigned int xms) {unsigned char i, j;while(xms--){ i 2;j 239;do{while (-…

【PyQt】17-日历控件

文章目录 前言一、代码二、运行结果总结 前言 固定格式的表述 日期的获取 一、代码 #Author &#xff1a;susocool #Creattime:2024/3/19 #FileName:40-日历控件 #Description: 日历控件的展示 import sys from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQ…

V-JEPA模型,非LLM另外的选择,AGI的未来:迈向Yann LeCun先进机器智能(AMI)愿景的下一步

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Redisson 分布式锁原理分析

Redisson 分布式锁原理分析 示例程序 示例程序&#xff1a; public class RedissonTest {public static void main(String[] args) {Config config new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6379"…

最新Java面试题2【2024初级】

下载链接&#xff1a;博主已将以上这些面试题整理成了一个面试手册&#xff0c;是PDF版的 互联网大厂面试题 1&#xff1a;阿里巴巴Java面试题 2&#xff1a;阿里云Java面试题-实习生岗 3&#xff1a;腾讯Java面试题-高级 4&#xff1a;字节跳动Java面试题 5&#xff1a;字…

Apache Dolphinscheduler - 无需重启 Master-Server 停止疯狂刷日志解决方案

记录的是一个 3.0 比较难搞的问题&#xff0c;相信不少使用过 3.0 的用户都遇到过 Master 服务中存在一些工作流或者任务流一直不停的死循环的问题&#xff0c;导致疯狂刷日志。不过本人到现在也没找到最关键的触发原因&#xff0c;只是看到一些连锁反应带来的结果…… 影响因素…

第十届教育技术前沿国际会议(ICFET 2024)即将召开!

ICFET 2024 | Malacca, MalaysiaInstallation Documentation for your Bootstrap Templatehttp://www.ICFET.org/ 组织单位&#xff1a; 会议主题&#xff1a; 整合教育技术 社交媒体和社交网络 语义网 3.0 播客播放视频讲座 播客向学生提供反馈 Wiki 和博客在高等教育中的…

MySQL最实用面试题(2024-3-14持续更新中)

MySQL篇面试题 一、介绍 ​ 这是由小龙同学自己总结领悟的mysql面试题的解析&#xff0c;也是面试宝典 二、题目 1.数据库三大范式&#xff1a; –作用&#xff1a; ​ 使表结构清晰&#xff0c;减少数据冗余&#xff08;简单讲就是重复&#xff09;&#xff0c;提高查询…

《探索AI辅助研发的未来之路》

在当今科技飞速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐渗透到各个领域&#xff0c;其中之一便是研发领域。AI辅助研发正以惊人的速度改变着我们对于创新和发现的理解。本文将从技术进展、行业应用、挑战与机遇、未来趋势、法规影响以及人才培养等方…

STP环路避免实验(华为)

思科设备参考&#xff1a;STP环路避免实验&#xff08;思科&#xff09; 一&#xff0c;技术简介 Spanning Tree Protocol&#xff08;STP&#xff09;&#xff0c;即生成树协议&#xff0c;是一种数据链路层协议。主要作用是防止二层环路&#xff0c;并自适应网络变化和故障…

Vue+SpringBoot打造民宿预定管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用例设计2.2 功能设计2.2.1 租客角色2.2.2 房主角色2.2.3 系统管理员角色 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿4.3 新增民宿评价4.4 查询留言4.5 新增民宿订单 五、免责说明 一、摘要 1.1 项目介绍 基于…

GPT-5:人工智能的下一个前沿即将到来

当我们站在人工智能新时代的门槛上时&#xff0c;GPT-5即将到来的呼声愈发高涨且迫切。作为革命性的GPT-3的继任者&#xff0c;GPT-5承诺将在人工智能领域迈出量子跃迁式的进步&#xff0c;其能力可能重新定义我们与技术的互动方式。 通往GPT-5之路 通往GPT-5的旅程已经标记着…

鸿蒙-自定义组件的生命周期

目录 自定义组件的生命周期 1.aboutToAppear 2.aboutToDisappear 3.onPageShow 4.onPageHide 5.onBackPress 日志输出 1.显示页面 2.页面点击返回按钮 3.页面跳转 4.页面返回 自定义组件的生命周期 先来一段列子 import router from ohos.router Entry Component…