Linux 线程互斥

1.相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;
struct customer
{pthread_t tid;string name;
};
int g_tickets = 1000;
void* GetTicket(void* args)
{customer* c = (customer*)args;while(true){if(g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;}elsebreak;}return nullptr;
}int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i].name= "customer-" + to_string(i + 1);pthread_create(&custs[i].tid, nullptr, GetTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i].tid, nullptr);}return 0;
}

例如上面代码中,我们模拟一个抢票系统,票 g_tickets 为全局变量,被所有线程共享,即为临界资源,而在所有线程进行的 GetTicket 函数中,都进行抢票,对 g_tickets 进行 -- 操作。下面访问临界资源 g_tickets 的代码区域就是临界区。

if(g_tickets > 0)
{usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;
}

随后我们执行代码

发现其竟然抢到了0,-1,-2,-3这些不存在的票,所以我们可以得知访问临界资源时是会出现问题的,于是我们可以引出以下概念:

  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成,即一条汇编语句。

上面抢票系统为什么会出现这样的情况呢?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • --ticket 操作本身就不是一个原子操作,即它是多条汇编语句组成

例如,我们简化模型:有customer - 1 和 customer - 2 去抢一张票

该线程先判断,发现 g_ticket > 0,于是进入第一个if语句,进行g_ticket--。但是g_ticket--本质上是多条汇编语句,比如下面这样:

MOV  eax, [0x1000]  ; 读取 g_ticket 的值
DEC  eax            ; 减 1
MOV  [0x1000], eax  ; 将值写回 g_ticket

第一行MOVE把内存中 g_ticket 的数据拷贝到CPU中的eax寄存器,第二行是将 eax中的值 - 1,第三行是将eax中的值拷贝回内存中的 g_ticket。

假设我们现在执行到 g_ticket-- 汇编的第二条指令,突然线程customer-1的时间片结束了,要结束调度当前线程,去调度customer-2了。此时内存中的g_ticket还没有被修改了,于是CPU保存当前线程customer-1的上下文,切换调度customer-2

此时线程customer-2也通过if (g_ticket > 0)检测发现还有一张票,于是custome-2也去抢这张票,执行g_ticket--。

这下出问题了,customer-1已经抢了这张票,但是还没来得及把g_ticket变成0,此时customer-2又进来抢了一次票。最后就会出现一张票被两个人抢到的问题。

要解决以上问题,需要做到三点:
  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

这就需要用锁来实现线程互斥,Linux上提供的这把锁叫互斥量。

2.互斥量mutex

初始化互斥量

初始化互斥量有两种方法:
方法 1 ,静态分配 :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
我们创建了一个名为 mutex的变量,类型是  pthread_mutex_t ,全局的互斥锁用宏PTHREAD_MUTEX_INITIALIZER 进行初始化, 并且使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
方法 2 ,动态分配 :
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数:
mutex:指向互斥量变量的指针,该变量将被初始化。
attr:指向互斥量属性的指针,如果为 NULL,则使用默认属性。

返回值:如果函数成功,返回 0;如果失败,返回相应的错误码。

方法3,C++STL:

在C++标准模板库(STL)中,可以使用std::mutex作为互斥量:

#include <mutex>int main() {std::mutex my_mutex;// 使用互斥量...// 不需要显式销毁,因为它是对象,在其作用域结束时会自动析构return 0;
}

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
  • mutex: 这是指向已经初始化过的互斥量的指针。

返回值:如果函数成功,则返回 0;若发生错误,则返回相应的错误码。

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

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • pthread_mutex_lock:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码。
  • pthread_mutex_trylock:用于非阻塞等待申请锁,如果申请失败,立即返回错误码EBUSY,表示互斥量当前不可用;如果申请成功,就执行临界区代码。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock:释放(解锁)互斥量mutex,使其他线程可以获取该互斥量,调用这个函数的线程应该是当前持有互斥量的线程。

接下来我们修改一下抢票系统的代码,给它加锁,保证抢票g_ticket--的原子性:

int g_tickets = 1000;
//定义一个全局变量的锁,保证所有线程都用同一个锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void *GetTicket(void *args)
{customer *c = (customer *)args;while (true){pthread_mutex_lock(&g_mutex); //加锁if (g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;pthread_mutex_unlock(&g_mutex); //解锁}else{pthread_mutex_unlock(&g_mutex); //解锁break;}}return nullptr;
}

当第一个线程被调度进行抢票,先申请锁g_mutex,然后再去if中访问g_ticket。假如在访问临界资源的过程中,CPU调度了第二个线程,第二个线程也想访问g_ticket,于是也申请锁g_mutex,但是由于锁已经被第一个线程申请走了,此时第二个线程pthread_mutex_lock就会失败,然后阻塞等待。等到第一个线程再次被调度,访问完临界区后,对g_mutex解锁,此时锁又可以被申请了。于是线程二申请到锁,再去访问g_ticket。加锁可以保证任何时候都只有一个线程访问临界区,这就保证了临界区的原子性,从而维护线程的安全!

3. 互斥量的实现原理

互斥量的汇编伪代码如下:

加锁lock

moveb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){return 0;
}else挂起等待;
goto lock

假设有这样的情形,有两个线程thread-1thread-2,它们共用内存中的锁mutex。在CPU中有一个寄存器%al,用于存储锁的值。

假设thread-1进行调度执行pthread_mutex_lock

首先执行指令moveb $0, %al,把寄存器%al内部的值变成0

随后执行xchgb %al, mutex,让内存中的mutex与寄存器%al的值进行交换:

此时寄存器%al的值变成1mutex的值变成0。随后执行:

if (al寄存器的内容 > 0){return 0;
}else挂起等待;

判断当前%al内部的值是否大于0,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功,否则执行else进行挂起等待。

现在假设thread-1执行到第一条汇编语句后,%al的值还是0,但是CPU切换去调度thread-2了:

现在thread-1保存自己的硬件上下文,包括%al = 0在内,随后therad-2进入:

现在thread-2执行了两行汇编语句,成功把内存中的mutex与自己的%al交换,申请到了锁,此时thread-1再次调度,thread-2拷贝走自己的硬件上下文:

恢复硬件上下文后,thread-1的%al等于0,执行第二条语句后,%al和mutex依然是0,这表明锁已经别的线程拿走了,此时在执行if内部的内容,thread-1挂起等待。

其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%al和mutex都是0,就算交换后还是0,也申请不到锁。并不是谁先调用ptherad_mutex_lock,谁就先抢到锁,而是谁先执行该函数内部的xchgb %al, mutex语句,把非零值放到自己的%al中,谁才抢到锁。

再简单看看解锁unlock

moveb $1, mutex
唤醒等待mutex的线程;
return 0;

moveb $1, mutex就是把%al中的1还给mutex,然后唤醒所有等待该锁的线程,让它们再次争夺这把锁。最后return 0,也就是pthread_mutex_unlock函数返回0

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

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

相关文章

mac安装jdk8

这里写自定义目录标题 一、下载JDK8二、安装JDK8三、配置环境变量3.1 找到JDK安装目录3.2 打开终端&#xff1a;3.3 输入如下配置&#xff1a;3.3 查看配置是否成功&#xff1a; 一、下载JDK8 oracle官网下载或从下面链接获取 https://download.csdn.net/download/qq_44107684…

【小沐学Golang】基于Go语言搭建静态文件服务器

文章目录 1、简介2、安装2.1 安装版2.2 压缩版 3、基本操作3.1 go run3.2 go build3.3 go install3.4 go env3.5 go module 4、文件服务器4.1 filebrowser4.2 gohttpserver4.3 goFile 5、FAQ5.1 go.mod 为空5.2 超时 结语 1、简介 https://golang.google.cn/ Go语言诞生于2007…

day02 -- docker

1.docker的介绍 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。容器是完全使…

●day 35 动态规划part01

第九章 动态规划part01 动态规划的类别 理论基础 动态规划下五步曲&#xff1a; 1、确定dp数组&#xff08;dp table&#xff09;以及下标的含义 2、确定递推公式 3、dp数组如何初始化 4、确定遍历顺序 5、打印dp数组 代码随想录 斐波那契数 代码随想录 动态规划5部曲 cla…

高级语言源程序转换为可执行目标文件

将高级语言源程序转换为可执行目标文件的过程通常包括以下几个主要步骤&#xff1a; ​ 1. 预处理&#xff08;Preprocessing&#xff09;&#xff1a; 由谁完成预处理器&#xff08;cpp&#xff09;操作处理源代码中的预处理指令&#xff08;如宏定义、文件包含、条件编译等&…

Linux——动态卷的管理

确保已经设置了对应的动态卷的驱动&#xff08;provisioner 制备器&#xff09;基于动态驱动创建对应的存储类创建PVC &#xff08;PVC 将会自动根据大小、访问模式等创建PV&#xff09;Pod的spec 中通过volumes 和 volumemounts 来完成pvc 的绑定和pvc对应pv的挂载删除pod 不…

Linux网络编程(七)-TCP协议客户端及代码实现

1.TCP的客户端代码流程简述 这一章将为大家讲解Socket通信中客户端的实现过程&#xff0c;还是先上图&#xff0c;请大家了解客户端的步骤 可以看到&#xff0c;相比服务端&#xff0c;客户端的步骤简单的很多。事实上这种情况比较多&#xff0c;比如一个服务端会有多个客户端…

JMeter模拟并发请求

PostMan不是严格意义上的并发请求工具&#xff0c;实际是串行的&#xff0c;如果需要测试后台接口并发时程序的准确性&#xff0c;建议采用JMeter工具。 案例&#xff1a;JMeter设置20个并发卖票请求&#xff0c;查看后台是否存在超卖的情况 方式一&#xff1a;一共10张票&…

TrickMo 安卓银行木马新变种利用虚假锁屏窃取密码

近期&#xff0c;研究人员在野外发现了 TrickMo Android 银行木马的 40 个新变种&#xff0c;它们与 16 个下载器和 22 个不同的命令和控制&#xff08;C2&#xff09;基础设施相关联&#xff0c;具有旨在窃取 Android 密码的新功能。 Zimperium 和 Cleafy 均报道了此消息。 …

编写一个通用的i2c控制器驱动框架

往期内容 I2C子系统专栏&#xff1a; I2C&#xff08;IIC&#xff09;协议讲解-CSDN博客SMBus 协议详解-CSDN博客I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客内核提供的通用I2C设备驱动I2c-dev.c分析&#xff1a;注册篇内核提供的通用I2C设备驱动I2C-dev.…

时空数据时序预测模型: HA、VAR、GBRT、GCN、DCRNN、FCCF、ST-MGCN

HA (Historical Average) HA (Historical Average&#xff0c;历史平均模型) 是一种基础的时间序列预测方法&#xff0c;通常用于预测具有周期性或季节性规律的数据。它通过计算历史上同一时间段的平均值来预测未来值&#xff0c;假设数据会遵循某种周期性的变化模式。以下是对…

智能家居的“眼睛”:计算机视觉如何让家更智能

引言 在不远的未来&#xff0c;当我们走进家门&#xff0c;灯光自动亮起&#xff0c;空调已经调至最舒适的温度&#xff0c;甚至音乐也播放着我们最喜欢的歌曲。 这一切&#xff0c;都得益于智能家居系统的发展。而在这个系统中&#xff0c;计算机视觉技术扮演着至关重要的角色…

SpringBoot车辆管理系统:构建与优化

4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式&#xff0c;是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示&#xff1a; 图4-1系统工作原理…

群晖通过 Docker 安装 MySQL

1. 打开 Docker 应用&#xff0c;并在注册表搜索 MySQL 2. 下载 MySQL 镜像&#xff0c;并选择版本 3. 在 Docker 文件夹中创建 MySQL&#xff0c;并创建子文件夹 4. 设置权限 5. 选择 MySQL 映像运行&#xff0c;创建容器 6. 配置 MySQL 容器 6.1 使用高权限执行容器 6.2 启…

scrapy 爬虫学习之【中医药材】爬虫

本项目纯学习使用。 1 scrapy 代码 爬取逻辑非常简单&#xff0c;根据url来处理翻页&#xff0c;然后获取到详情页面的链接&#xff0c;再去爬取详情页面的内容即可&#xff0c;最终数据落地到excel中。 经测试&#xff0c;总计获取 11299条中医药材数据。 import pandas as…

idea 2023 配置 web service

前言 能在网上查到的资料,都是比较老的,搞了一上午才配置好了环境 因此记录一下,服务你我他 我的环境: java 1.8,Idea2023.1 配置web service 服务端 直接新建一个java新项目 下载插件 添加框架支持 启动项目 配置web service 客户端 新建项目,下载三个插件的步骤和上面服务…

JMeter之mqtt-jmeter 插件介绍

前言 mqtt-jmeter插件是JMeter中的一个第三方插件&#xff0c;用于支持MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;协议的性能测试。MQTT是一种轻量级的发布/订阅消息传输协议&#xff0c;广泛应用于物联网和传感器网络中。 一、安装插件 mqtt-jmeter项目…

【Hive】6-Hive函数、运算符使用

Hive函数、运算符使用 Hive内置运算符 概述 整体上&#xff0c;Hive支持的运算符可以分为三大类&#xff1a;关系运算、算术运算、逻辑运算。 官方参考文档&#xff1a;https://cwiki.apache.org/confluence/display/Hive/LanguageManualUDF 也可以使用下述方式查看运算符的…

2024年AI 制作PPT新宠儿,3款神器集锦,让你的演示与众不同

咱们今儿聊聊最近超火的AI做PPT的工具。这年头&#xff0c;谁不想省事儿&#xff0c;少熬夜加班&#xff0c;多享受享受生活啊&#xff1f;所以&#xff0c;AI开始帮咱们搞定做PPT这种费时的活儿&#xff0c;我自然得好好研究研究。今天&#xff0c;我就给大家详细说说三款很火…

Linux下的进程解析(level 2)

目录 引言 pid解析 /proc 系统调用 fork &#xff1a;创建子进程 执行流分析 父子进程谁先运行 引言 在当今的信息技术时代&#xff0c;操作系统作为计算机系统的核心组件&#xff0c;承担着资源管理、任务调度等重要职责。Linux作为一种开源、高性能的操作系统&#xf…