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

Linux:线程互斥

线程互斥

先看到一个抢票案例:

class customer
{
public:int _ticket_num = 0;pthread_t _tid;string _name;
};int g_ticket = 10000;void* buyTicket(void* args)
{customer* cust = (customer*)args;while(true){if(g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;}else{break;}}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, buyTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i]._tid, nullptr);}for(int i = 0; i < 5; i++){cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

输出结果: 

g_ticket--本质上是多条汇编语句,比如下面这样:

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

为什么最开始的案例中,会出现10005张票,就是因为最后一张票,被五个线程同时抢到了!当g_ticket已经被抢走时,由于没来得及g_ticket = 0,导致后来的线程以为还有票。

引入一部分概念,方便理解后续知识:

  • 临界资源:以上案例中,g_ticket是共享资源,多个线程共享。我们把这种资源称为临界资源
  • 临界区:访问临界资源的代码,叫做临界区。比如g_ticket--就是临界区代码,以为其访问了临界资源g_ticket
  • 原子性:表示一个操作对外表现只有两种状态:还没开始已经结束

我们说g_ticket--本质上会变成多条汇编语句,也就是说g_ticket--是有过程的,而不是一瞬间完成的。

这就导致在我还没有完成g_ticket--的时候,也就是在g_ticket--过程中,被其他线程打断了。导致其它线程收到错误的信息,抢到不存在的票。

如果说一个线程抢到票后,g_ticket--会立马执行完毕,下一个线程在访问这个g_ticket > 0的时候,一定是在别人已经g_ticket--完毕,而不是在g_ticket--过程中,就可以避免这个问题。这就要求访问g_ticket是原子性的,也就是说在别的线程眼中,根本就不存在g_ticket--的过程,要么你没有执行g_ticket--,要么已经执行完毕。

临界区代码只要保证是原子性的,就可以避免这样线程之间错误的抢占资源相同资源的问题

 那么要如何保证临界区代码是原子性的呢?此时就需要线程互斥了!

线程互斥指的是在多线程环境中,多个线程访问同一个共享资源时,只允许一个线程访问,其他线程必须等待,直到当前线程访问完成才能继续访问。

线程互斥,是通过来实现的。

锁的规则如下:

  1. 代码必须要有互斥行为:当代码进入临界区时,不允许其它线程进入临界区
  2. 如果多个线程都想执行临界区代码,并且当前临界区没有线程在执行代码,只允许一个线程进入临界区
  3. 线程不能阻止其他线程进入临界区

简单来说就是:任何时候临界区都只能有一个线程执行!


互斥锁 mutex

互斥锁pthread库提供的,英文名为mutex(互斥),需要头文件<pthread.h>

互斥锁的类型是pthread_mutex_t,分为全局互斥锁局部互斥锁,它们的创建方式不同。

全局mutex

想要创建一个全局的互斥锁很简单,直接定义即可:

pthread_mutex_t xxx = PTHREAD_MUTEX_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_mutex_t,即这个变量是一个互斥锁全局的互斥锁必须用宏PTHREAD_MUTEX_INITIALIZER进行初始化! 

另外,全局的互斥锁不需要手动销毁。

局部mutex

局部的互斥锁是需要通过接口来初始化与销毁的,接口如下:

pthread_mutex_init

pthread_mutex_init函数用于初始化一个互斥锁,函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *res

参数:

  • restrict mutex:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,对其初始化
  • restrict attr:用于设定该互斥锁的属性,一般不用,设为空指针即可

返回值:成功返回0;失败返回错误码


pthread_mutex_destroy

pthread_mutex_destroy函数用于销毁一个互斥锁,函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,销毁该锁

返回值:成功返回0;失败返回错误码


创建好互斥锁后,就要使用这个锁,主要是两个操作:申请锁释放锁

三个函数的原型

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码
  • pthread_mutex_trylock:用于申请锁,如果申请失败,直接返回,而不是等待;如果申请成功,就执行临界区代码
  • pthread_mutex_unlock:用于释放锁,表明自己已经访问完毕临界区,其他线程可以来访问了

修改一下最初的抢票代码,给它加锁,保证抢票g_ticket--的原子性:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局互斥锁void *buyTicket(void *args)
{customer *cust = (customer *)args;while (true){pthread_mutex_lock(&mutex); // 加锁if (g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;pthread_mutex_unlock(&mutex); // 解锁cust->_ticket_num++;}else{pthread_mutex_unlock(&mutex); // 解锁break;}}return nullptr;
}

 


互斥锁原理

那么互斥锁是如何做到的呢?

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

加锁lock

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

         

如图所示,现在有两个线程thread-1和thread-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,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功。否则执行else进行挂起等待。

这样一个线程就征用到了一把锁。

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

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

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

可以看到,其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%almutex都是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


常见的锁

Linux中不仅仅存在互斥锁这一种锁,还有非常多的锁,接下来我们看看其它的锁。

死锁

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

 现在有两个线程thread-1thread-2,以及两把互斥锁mutex-1mutex-2

现在要求:一个线程想要访问临界资源,必须同时持有mutex-1mutex-2。随后therad-1去申请了mutex-1thread-2去申请了mutex-2 thread-1再去申请mutex-2,结果mutex-2已经被therad-2占用了,thread-1陷入阻塞:

thread-2再去申请mutex-1,结果mutex-1已经被therad-1占用了,thread-2陷入阻塞:         

想要造成死锁,有四个必要条件:

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

以上是比较正式的说法,接下来我从线程角度简单翻译翻译:

  1. 互斥条件临界资源同时只能被一个线程访问=
  2. 请求与保持条件:请求是指:申请对方的锁,保持是指:占着自己有的锁不放
  3. 不剥夺条件:一个线程如果申请锁失败,强行抢走他人的锁
  4. 循环等待条件:以刚刚的死锁为例,therad-1thread-2,而thread-2等待thread-1,形成一个头尾相接的循环

这四个条件都是必要条件,也就是说:

解决死锁,本质就是破坏一个或多个必要条件

主要有以下方式避免死锁:

  1. 破坏互斥条件:不要用锁
  2. 破坏请求与保持条件:如果发现没有申请到锁,立刻释放自己的全部锁
  3. 破坏不剥夺条件:如果发现没有申请到锁,强行释放对方的锁,将其占为己有
  4. 破坏循环等待条件:如果申请多把锁,所有线程都必须按照相同的顺序申请(最简单的方式)

自旋锁 spinlock

我们先前讲的锁,其机制是这样的:

当线程申请一个锁失败,就会阻塞等待,当锁被使用完毕,唤醒所有等待该锁的线程。

其实锁还有一种不用阻塞等待的策略,而是反复检测的策略,就像这样:

 

当线程没有申请到锁,一段时间后再次检测这个锁有没有被释放,一直反复申请这个锁,这个过程叫做自旋。基于这个策略来申请的锁,叫做自旋锁

Linux自带了自旋锁spinlock,类型为pthread_spinlock_t,接口如下:

创建与销毁

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁与解锁

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

你会发现,这和mutex几乎一摸一样,所以接口也就不讲解了。

不过我这里要强调一点,pthread_spin_lock并不是申请失败就返回,而是在pthread_spin_lock内部以自旋的方式申请锁,我们无需手动模拟自旋的过程。

 

 

 

 

 

 

 

        

 

 

 

 

 

 

 

 

 

 

相关文章:

Linux:线程互斥

线程互斥 先看到一个抢票案例&#xff1a; class customer { public:int _ticket_num 0;pthread_t _tid;string _name; };int g_ticket 10000;void* buyTicket(void* args) {customer* cust (customer*)args;while(true){if(g_ticket > 0){usleep(1000);cout << …...

misc流量分析

一、wireshark语法 1、wireshark过滤语法 &#xff08;1&#xff09;过滤IP地址 ip.srcx.x..x.x 过滤源IP地址 ip.dstx.x.x.x 过滤目的IP ip.addrx.x.x.x 过滤某个IP &#xff08;2&#xff09;过滤端口号 tcp.port80tcp.srcport80 显示TCP的源端口80tcp.dstport80 显示…...

Linux驱动(五):Linux2.6驱动编写之设备树

目录 前言一、设备树是个啥&#xff1f;二、设备树编写语法规则1.文件类型2.设备树源文件&#xff08;DTS&#xff09;结构3.设备树源文件&#xff08;DTS&#xff09;解析 三、设备树API函数1.在内核中获取设备树节点&#xff08;三种&#xff09;2.获取设备树节点的属性 四、…...

算法【Java】 —— 前缀和

模板引入 一维前缀和 https://www.nowcoder.com/share/jump/9257752291725692504394 解法一&#xff1a;暴力枚举 在每次提供 l 与 r 的时候&#xff0c;都从 l 开始遍历数组&#xff0c;直到遇到 r 停止&#xff0c;这个方法的时间复杂度为 O(N * q) 解法二&#xff1a;前…...

python网络爬虫(四)——实战练习

0.为什么要学习网络爬虫 深度学习一般过程:   收集数据&#xff0c;尤其是有标签、高质量的数据是一件昂贵的工作。   爬虫的过程&#xff0c;就是模仿浏览器的行为&#xff0c;往目标站点发送请求&#xff0c;接收服务器的响应数据&#xff0c;提取需要的信息&#xff0c…...

tio websocket 客户端 java 代码 工具类

为了更好地组织代码并提高可复用性&#xff0c;我们可以将WebSocket客户端封装成一个工具类。这样可以在多个地方方便地使用WebSocket客户端功能。以下是使用tio库实现的一个WebSocket客户端工具类。 1. 添加依赖 确保项目中添加了tio的依赖。如果使用的是Maven&#xff0c;可以…...

通过卷积神经网络(CNN)识别和预测手写数字

一&#xff1a;卷积神经网络&#xff08;CNN&#xff09;和手写数字识别MNIST数据集的介绍 卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是一种深度学习模型&#xff0c;它在图像和视频识别、分类和分割任务中表现出色。CNN通过模仿…...

【A题第二套完整论文已出】2024数模国赛A题第二套完整论文+可运行代码参考(无偿分享)

“板凳龙” 闹元宵路径速度问题 摘要 本文针对传统舞龙进行了轨迹分析&#xff0c;并针对一系列问题提出了解决方案&#xff0c;将这一运动进行了模型可视化。 针对问题一&#xff0c;我们首先对舞龙的螺线轨迹进行了建模&#xff0c;将直角坐标系转换为极坐标系&#xff0…...

一份热乎的数据分析(数仓)面试题 | 每天一点点,收获不止一点

目录 1. 已有ods层⽤⼾表为ods_online.user_info&#xff0c;有两个字段userid和age&#xff0c;现设计数仓⽤⼾表结构如 下&#xff1a; 2. 设计数据仓库的保单表&#xff08;⾃⾏命名&#xff09; 3. 根据上述两表&#xff0c;查询2024年8⽉份&#xff0c;每⽇&#xff0c…...

3 html5之css新选择器和属性

要说css的变化那是发展比较快的&#xff0c;新增的选择器也很多&#xff0c;而且还有很多都是比较实用的。这里举出一些案例&#xff0c;看看你平时都是否用过。 1 新增的一些写法&#xff1a; 1.1 导入css 这个是非常好的一个变化。这样可以让我们将css拆分成公共部分或者多…...

【Kubernetes】K8s 的鉴权管理(一):基于角色的访问控制(RBAC 鉴权)

K8s 的鉴权管理&#xff08;一&#xff09;&#xff1a;基于角色的访问控制&#xff08;RBAC 鉴权&#xff09; 1.Kubernetes 的鉴权管理1.1 审查客户端请求的属性1.2 确定请求的操作 2.基于角色的访问控制&#xff08;RBAC 鉴权&#xff09;2.1 基于角色的访问控制中的概念2.1…...

保研 比赛 利器: 用AI比赛助手降维打击数学建模

数学建模作为一个热门但又具有挑战性的赛道&#xff0c;在保研、学分加分、简历增色等方面具有独特优势。近年来&#xff0c;随着AI技术的发展&#xff0c;特别是像GPT-4模型的应用&#xff0c;数学建模的比赛变得不再那么“艰深”。通过利用AI比赛助手&#xff0c;不仅可以大大…...

秋招校招,在线性格测评应该如何应对

秋招校招&#xff0c;如果遇到在线测评&#xff0c;如何应对&#xff1f; 这里写个总结稿&#xff0c;希望对大家有些帮助。在线测评是企业深入了解求职人的渠道&#xff0c;如果是性格测试&#xff0c;会要求测试者能够快速答出&#xff0c;以便于反应实际情况&#xff08;时间…...

chrome 插件开发入门

1. 介绍 Chrome 插件可用于在谷歌浏览器上控制当前页面的一些操作&#xff0c;可自主控制网页&#xff0c;提升效率。 平常我们可在谷歌应用商店中下载谷歌插件来增强浏览器功能&#xff0c;作为开发者&#xff0c;我们也可以自己开发一个浏览器插件来配合我们的日常学习工作…...

揭开面纱--机器学习

一、人工智能三大概念 1.1 AI、ML、DL 1.1.1 什么是人工智能? AI&#xff1a;Artificial Intelligence 人工智能 AI is the field that studies the synthesis and analysis of computational agents that act intelligently AI is to use computers to analog and instead…...

Python中的私有属性与方法:解锁面向对象编程的秘密

在Python的广阔世界里&#xff0c;面向对象编程&#xff08;OOP&#xff09;是一种强大而灵活的方法论&#xff0c;它帮助我们更好地组织代码、管理状态&#xff0c;并构建可复用的软件组件。而在这个框架内&#xff0c;私有属性与方法则是实现封装的关键机制之一。它们不仅有助…...

开篇_____何谓安卓机型“工程固件” 与其他固件的区别 作用

此系列博文将分析安卓系列机型与一些车机 wifi板子等工程固件的一些常识。从早期安卓1.0起始到目前的安卓15&#xff0c;一些厂家发布新机型的常规流程都是从工程机到量产的过程。在其中就需要调试各种参数以便后续的量产参数可以固定到最佳&#xff0c;工程固件由此诞生。 后…...

DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed

DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed 文章目录 DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed问题解决办法 问题 使用 DBeaver 连接 MySQL 数据库的时候&#xff0c; 一直报错下面的错误 Public Key Retrieval is not allowed详细…...

三个月涨粉两万,只因为知道了这个AI神器

大家好&#xff0c;我是凡人&#xff0c;最近midjourney的账号到期了&#xff0c;正准备充值时&#xff0c;被一个国内AI图片的生成神器给震惊了&#xff0c;不说废话&#xff0c;先上图看看生成效果。 怎么样还不错吧&#xff0c;是我非常喜欢的国风画&#xff0c;哈哈&#x…...

vulhub GhostScript 沙箱绕过(CVE-2018-16509)

1.搭建环境 2.进入网站 3.下载包含payload的png文件 vulhub/ghostscript/CVE-2018-16509/poc.png at master vulhub/vulhub GitHub 4.上传poc.png图片 5.查看创建的文件...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?

一、核心优势&#xff1a;专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发&#xff0c;是一款收费低廉但功能全面的Windows NAS工具&#xff0c;主打“无学习成本部署” 。与其他NAS软件相比&#xff0c;其优势在于&#xff1a; 无需硬件改造&#xff1a;将任意W…...

Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务

通过akshare库&#xff0c;获取股票数据&#xff0c;并生成TabPFN这个模型 可以识别、处理的格式&#xff0c;写一个完整的预处理示例&#xff0c;并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务&#xff0c;进行预测并输…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

视频字幕质量评估的大规模细粒度基准

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用&#xff0c;因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型&#xff08;VLMs&#xff09;在字幕生成方面…...

VTK如何让部分单位不可见

最近遇到一个需求&#xff0c;需要让一个vtkDataSet中的部分单元不可见&#xff0c;查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行&#xff0c;是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示&#xff0c;主要是最后一个参数&#xff0c;透明度…...

拉力测试cuda pytorch 把 4070显卡拉满

import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试&#xff0c;通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小&#xff0c;增大可提高计算复杂度duration: 测试持续时间&#xff08;秒&…...