linux线程 | 一点通你的互斥锁 | 同步与互斥
前言:本篇文章主要讲述linux线程的互斥的知识。 讲解流程为先讲解锁的工作原理, 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西, 但博主会总结两条结论!!最后就是讲一下死锁。 那么, 废话不多说, 我们开始学习吧!
ps:本节内容建议先了解一下数据不一致问题以及锁的使用的友友们进行观看哦。
目录
锁的原理
锁的封装
接口
测试
可重入VS线程安全
概念
结论
死锁的概念
要如何解决死锁问题呢?
我们回忆一下上一节, 上一节我们知道了:为了提高并发度, 所以有了多线程。 为了使用多线程, 我们就有了线程之间的资源共享。 而资源共享又引入了多线程的访问数据不一致的问题。 因为数据不一致问题, 我们又引入了互斥锁, 而互斥锁需要考虑临界资源和临界区以及原子性和互斥性的概念。——这些就是上一节的大体内容。 现在看本节的内容吧!
锁的原理
我们现在来重新思考一下一个问题——tickets--为什么不是原子的? 因为汇编会变成三条汇编语句。 我们认为一条汇编语句已经是计算机中最基本的指令了。什么是原子的? 这里可以下一个定义: 我们认为, 一条汇编, 就是原子的。
其实, cpu这个硬件资源是很笨的, 我们让他去move, sub, add。 他就按照我们的指令去做。 这里面就有一个问题, 就是cpu很笨, 我们让他干什么, 他就干什么。 但是cpu为什么知道我们让他去干什么呢? 是因为我们对应的芯片当年在制作的时候, 他得在自己的芯片的硬件电路离, 一定要以硬件的方式设计出一系列能够让硬件识别的基本指令。 这个基本指令叫做芯片的指令集(区别于代码, 就是我们的汇编move, sub, add这些)
为了实现互斥操作, 大多数体系结构提供了swap和exchange指令。 这些指令比如swap,那么他就是一条利用汇编把寄存器里面的值和内存里面的值做交换。 由于只有一条指令, 所以就保证了交换的动作是原子的。 即便是多处理器平台(多个cpu), 我们需要知道的是, 即便我们的cpu有很多块, 但是我们的cpu和内存之间的总线只有一套。所以这么多cpu通过总线访问内存的时候就会通过总线里面的硬件——仲裁器, 来决定内存由哪一个cpu来访问。 也就是说所有的cpu在访问内存的时候还是串行的, 只是在计算的时候大家可以在双cpu或者多cpu下进行计算。
现在看下面一串伪代码, 这串伪代码是mutex_lock的伪汇编:
move $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){return 0;
} else 挂起等待;
goto lock;
这串伪代码怎么理解, 下面博主带友友们理解一下:
- 图中是一块cpu和一块物理内存。 物理内存中有一块锁, 锁里面的数据就代表锁!我们设置为1.
- 然后第一条语句是move 0 al。意思就是将0赋值给al变量(al此时已经被加载到寄存器)。
- 第二条语句是xchgb al mutex, 意思就是将al 和 mutex中的数据调换。这也是传统意义上申请锁的动作。然后调换后就是下图:
- 第三条语句就是对al寄存器里面的数据进行判断。 如果寄存器数据大于0。 那么就申请成功, 返回零。 如果小于零, 就申请失败, 挂起等待。
但是!这是只有一个线程的情况, 我们可以一步一步地向后执行指令。 当有多个线程的时候就要考虑线程切换的问题了。 下面是多线程的情况(注意:寄存器 != 寄存器的内容):
- 假设一开始线程1执行第一条语句, 将al赋值为了零。 然后线程1就要被切换走了。
- 那么此时线程1就要将寄存器中的al数据记录下来,同时将下一步执行哪一步记录下来。这叫带走上下文!
- 然后呢线程2来了, 线程2开始将数据加载到寄存器, 然后al 置为0.但是线程2运气好, 他没有被切换走, 然后他就继续执行xchgb操作。 成功的将al的数据0和mutex的数据1完成了交换! 此时现成2终于要被切换走了!那么现成2就要将自己的上下文带走!!!
- 线程2切换走了之后,线程1回来执行xchgb, 但是此时mutex的值已经是0了,线程1交换al 和 mutex相当于什么都没有交换到! 然后执行判断, 对不起, 判断失败, 线程1需要挂起等待。
- 那么线程2回来之后继续执行判断, 注意, 此时原本的mutex的那个1就在线程2的上下文中!那么线程2判断结果一定会正确, 那么线程2就申请成功了锁!
所以, 综上我们其实就能感觉出来, 上面的mutex_lock函数那哪一步最重要? 是不是就是xchgb这一步最重要。 谁先交换成功, 谁就相当于拿到了锁!!
锁的封装
接口
接下来我们对锁进行一下封装。 让锁的接口不再暴露在外面, 用户直接使用我们的接口:
首先我们创建一个类,这个类对锁进行了一下封装:
class Mutex //创建锁的类
{
public:private:pthread_mutex_t* _lock;
};
然后定义这个类的构造函数, 析构函数;以及加锁解锁函数:
class Mutex
{
public://构造函数Mutex(pthread_mutex_t* lock):_lock(lock){} //加锁void Lock(){pthread_mutex_lock(_lock);}//解锁void Unlock(){pthread_mutex_unlock(_lock);}//析构函数~Mutex(){}private:pthread_mutex_t* _lock;
};
然后我们创建一个类似于“开关”的对象。 这个对象只要创建, 就代表我们的加锁;这个对象一小会就代表我们的解锁!如下这里面封装了我们上面刚刚定义的Mutex类的对象。
class LockGuard
{
public:private:Mutex _mutex;
};
那么如何实现这种“资源创建即初始化”的效果呢?我们可以在它的构造函数初始化_mutex成员变量并且使用_mutex的方法lock。然后析构函数就是调用_mutex的unlock
class LockGuard
{
public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.Lock();}//~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
};
测试
有了上面的代码, 我们可以拿出我们之前写的买票的代码(不知道的友友请看之前一篇文章:linux线程 | 把握线程的知识要点 | 同步与互斥-CSDN博客, 也可以看下面代码):
#include<iostream>
using namespace std;
#include<pthread.h>
#include<vector>
#include<unistd.h>
#include<string.h>
#include"LockGuard.h"#define NUM 5 //创建多个执行流, NUM为执行流个数using namespace std;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int* p = nullptr;
//线程的数据信息。
struct threadData
{
public:threadData(int number, pthread_mutex_t* mutex){threadname = "thread-" + to_string(number);lock = mutex;}public:string threadname;pthread_mutex_t* lock;
};int tickets = 1000;
void* getTicket(void* args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock); //加锁, 申请锁成功才能往后执行, 否则阻塞等待。if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock); }else {pthread_mutex_unlock(td->lock); break;} usleep(13);}return nullptr;
}int main()
{ pthread_mutex_init(&lock, nullptr);vector<pthread_t> tids;vector<threadData*> thread_datas;//我们创建多个执行流, 为了能够验证每个线程都有一个独立的栈结构for (int i = 0; i < NUM; i++){pthread_t tid;threadData* td = new threadData(i, &lock);thread_datas.push_back(td); pthread_create(&tid, nullptr, getTicket, td);tids.push_back(tid);}for (auto e : tids){pthread_join(e, nullptr);}// pthread_mutex_destroy(&lock);return 0;
}
然后我们如何改动上面的代码呢? 就是换一下GetTickets函数里面的锁的创建方法, 改成直接一个LockGurard类型的对象:
void* getTicket(void* args)
{threadData* td = static_cast<threadData*>(args);const char* name = td->threadname.c_str();while (true){{ //这个花括号是为了规定LockGuard对象的作用域LockGuard lockguard(&lock); //定义临时的lockguard对象。 RAII风格的锁if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets);tickets--;}else {break;} }usleep(13);}return nullptr;
}
然后运行:
可以看到, 和原本创建锁的结果一样。
可重入VS线程安全
概念
如果多个线程同时访问一段代码的时候, 不管如何运作, 那么我们的线程都不会出现问题, 那么就称为线程安全。如果我们的代码访问某些全局变量, 然后导致其他线程出现问题了, 这个就叫做线程不安全。
重入就是同一个函数被多个执行流调用, 正在被一个执行流调用的时候, 其他的执行流又进来了。 然后在这种情况下仍然没有出现问题的函数称为可重入函数, 否则称为不可重入函数。
线程安全和重入这两个概念是一样的概念吗?不是的, 因为重入不可重入描述的是函数的特点, 而线程安全描述的是多线程并发的特点。目前, 我们遇到的大部分函数都是不可重入的。一个函数可重入或者不可重入, 其实并没有褒贬之分,他只是在描述这个函数的特征。但是!对于不可重入的函数我们的多执行流要用, 就必须要加锁了!!!
因为这里有一个结论性的话:一般情况下, 一个函数是不可能重入的,那么在多线程执行下, 就有可能出现问题。 但是, 如果一个函数是可重入的, 那么在多线程的调用下, 它一定也是线程安全的。
结论
常见的线程不安全的情况:
- 不保护共享变量的函数。
- 函数状态随着被调用, 状态发生变化的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
常见的线程安全的情况:
- 每个线程对于全局变量或者静态变量只读!
- 类或者接口原子的!
- 多个线程之间的切换不会导致接口结果产生二义性!
常见不可重入的情况:
- IO
- malloc/free函数等等函数
- 可重入函数体内使用了静态数据结构
可重入与线程安全的联系(重要)
- 函数如果是可重入的, 那么多线程调用这个函数的过程, 就是线程安全的!
- 函数是不可重入的, 那么一般不可多线程使用, 有可能引发线程安全问题。
- 如果一个函数中使用了全局变量。那么这个函数既不是线程安全的, 也不是可重入的。
可重入与线程安全的区别:
- 可重入是线程安全函数的一种
- 可重入一定是线程安全的,但是线程安全, 不一定是可重入的。
上面最最重要的就是两个结论:
- 线程安全是描述线程并发的问题, 可重入是描述函数特点的问题。
- 不可重入函数在多线程访问时可能会出现线程安全问题, 但是一个函数如果可重入, 他不会有线程安全问题。
死锁的概念
首先什么是死锁呢? ——一般在多线程访问的时候, 我们一方面持有自己的锁, 还申请其他人的锁。我们双方既不释放自己的锁, 而且要求释放对方的锁, 并且还不强占对方的锁, 只是申请对方的锁。 这就会导致死锁的问题。 以现象定义就是在多线程的情况下, 因为锁的使用导致多线程的代码都不往后执行了。
那么问题是, 一把锁可不可能产生死锁呢?——是可以的。当我们的同一把锁被申请两次的时候,就会产生死锁,就如同下面的代码:
运行的时候就会阻塞住了:
我们称一个线程持有一把锁, 另一个线程持有另一把锁。 他们两个却又申请对方的锁进而导致进入一种永久等待状态的情况我们称之为死锁。 就比如张三和李四买棒棒糖, 但是张三和李四兜里只有五毛钱, 棒棒糖要一块钱, 所以张三就让李四把五毛钱给他, 他给两个人买一根棒棒糖。 那李四不愿意, 李四就和张三说把五毛钱给他, 他给两个人买棒棒糖。 这两个小朋友一不释放自己手中的五毛钱, 而还一直要对方的五毛钱。这种情况叫做张三和李四陷入了死锁问题!
那么产生死锁的必要条件有什么呢? ——》只要产生死锁, 这四个条件一定产生!!!
- 1:互斥条件——》一个资源一次只能被一个执行流使用。——前提
- 2:请求与保持条件——》所谓死锁就是在互相保持自己的资源, 同时还在申请着对方的资源。——原则
- 3:不剥夺条件——就类似于张三想要对方的五毛钱,但是他不去抢。 一个执行流以获得的资源在未使用完之前,不能强行掠夺。——原则
- 4:循环等待条件——》若干执行流之间形成的一种头尾相连的循环等待资源的关系。 就类似于张三等待李四, 李四等待张三。——重要条件
要如何解决死锁问题呢?
我们知道, 对于死锁来说有四个必要的条件。 也就是说死锁必须有着这四个条件。 那么我们如果想要破坏死锁, 那么是不是只需要破坏掉其中某一个条件就可以了?
所以方法就是:
- 破坏请求与保持——》我们可以使用trylock, 它是lock的非阻塞版本, 很容易就能破坏请求与保持原则。
- 破坏不剥夺——》只需要将对方的锁释放掉, 就能破坏掉不剥夺条件。
- 破坏循环与等待条件——》破坏掉这个后, 申请锁的时候按照顺序申请锁!一般加锁的顺序保持一致!
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!
相关文章:

linux线程 | 一点通你的互斥锁 | 同步与互斥
前言:本篇文章主要讲述linux线程的互斥的知识。 讲解流程为先讲解锁的工作原理, 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西, 但博主会总结两条结论!!最后就是讲一下死锁。 那么, 废…...
全栈开发小项目
用到的技术栈: nodejswebpackknockoutmongodbPM2rabbitmq 以下是一个综合指南,展示如何将 Node.js、Webpack、Knockout.js、MongoDB、PM2 和 RabbitMQ 集成到一个项目中。 我们将在这一项目中添加 RabbitMQ,用于处理消息队列。这对于任务分…...
批处理一键创建扫描仪桌面打开快捷方式图标 简单直接有效 扫描文档图片的应急策略
办公生活中,我们在安装完多功能一体机的打印驱动之后,找不到扫描文件的地方,如果驱动程序安装正确,我们可以用系统自带的扫描仪程序调用这种打印机或复印机的扫描程序即可,它在电脑系统中的位置一般是:C:\W…...

【服务器知识】Tomcat简单入门
文章目录 概述Apache Tomcat 介绍主要特性版本历史使用场景 核心架构Valve机制详细说明请求处理过程 Tomcat安装Windows系统下Tomcat的安装与配置:步骤1:安装JDK步骤2:下载Tomcat步骤3:解压Tomcat步骤4:配置环境变量&a…...
【前端】Matter:过滤与高级碰撞检测
在物理引擎中,控制物体的碰撞行为是物理模拟的核心之一。Matter.js 提供了强大的碰撞检测机制和碰撞过滤功能,让开发者可以控制哪些物体能够相互碰撞,如何处理复杂的碰撞情况。本文将详细介绍 碰撞过滤 (Collision Filtering) 与 高级碰撞检测…...

wps图标没有坐标轴标题怎么办?wps表格不能用enter下怎么办?
目录 wps图标没有坐标轴标题怎么办 一、在WPS PPT中添加坐标轴标题 二、在WPS Excel中添加坐标轴标题 wps表格不能用enter下怎么办 一、检查并修改设置 二、检查单元格保护状态 三、使用快捷键实现换行 wps图标没有坐标轴标题怎么办 一、在WPS PPT中添加坐标轴标题 插入…...

在ESP-IDF环境中如何进行多文件中的数据流转-FreeRTOS实时操作系统_流缓存区“xMessageBuffer”
一、建立三个源文件和对应的头文件 建立文件名,如图所示 图 1-1 二、包含相应的头文件 main.h 图 2-1 mess_send.h mess_rece.h和这个中类似,不明白的大家看我最后面的源码分享 图2-2 三、声明消息缓存区的句柄 大家注意,在main.c中定义的是全局变…...
ConcurrentLinkedQueue适合什么样的使用场景?
ConcurrentLinkedQueue 是 Java 中一种无界线程安全的队列,适合多线程环境中的高并发场景。以下是一些它特别适合的使用场景: 1. 高频读操作,低频写操作 ConcurrentLinkedQueue 对于实际应用中读操作相对频繁,写操作较少的场景非…...

C语言 | Leetcode C语言题解之第480题滑动窗口中位数
题目: 题解: struct Heap {int* heap;int heapSize;int realSize;bool (*cmp)(int, int); };void init(struct Heap* obj, int n, bool (*cmp)(int, int)) {obj->heap malloc(sizeof(int) * (n 1));obj->heapSize 0;obj->cmp cmp; }bool c…...

LabVIEW开发如何实现降维打击
在LabVIEW开发中实现“降维打击”可以理解为利用软件优势和高效工具来解决复杂的问题,将多维度、多层次的技术简化为容易操作和管理的单一维度,达到出其不意的效果。以下是几种关键策略: 1. 模块化设计与封装 将复杂系统分解为若干模块&…...
docker 文件目录迁移
文章参考 du -hs /var/lib/docker/ 命令查看磁盘使用情况。 du -hs /var/lib/docker/docker system df命令,类似于Linux上的df命令,用于查看Docker的磁盘使用情况: rootnn0:~$ docker system df TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 7 2 122.2…...
Markdown 标题
Markdown 标题 Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式化的HTML代码。Markdown 的语法简洁明了,广泛用于撰写文档、博客文章、笔记等。本文将详细介绍 Markdown 的标题语法及其在文档中的应用。 Markdown 标题语法 在…...

【动手学电机驱动】TI InstaSPIN-FOC(5)Lab04 力矩控制
TI InstaSPIN-FOC(1)电机驱动和控制测试平台 TI InstaSPIN-FOC(2)Lab01 闪灯实验 TI InstaSPIN-FOC(3)Lab03a 测量电压电流漂移量 TI InstaSPIN-FOC(4)Lab02b 电机参数辨识 TI Insta…...
Mysql的CommunicationsException
一、报错内容 com.mysql.cj.jdbc.exceptions.CommunicationsException: The last packet successfully received from the server was 1,500,378 milliseconds ago. The last packet sent successfully to the server was 1,500,378 milliseconds ago. is longer than the s…...
C++学习笔记----9、发现继承的技巧(二)---- 重用目的的继承
现在你对继承的基本语法已经比较熟悉了,是时候探索继承是c语言中重要属性的一个主要原因了。继承是一个装备允许你平衡既有代码。本节会举出基于代码重用目的的继承的例子。 1、WeatherPrediction类 假想你有一个任务,写一个程序来发出简单的天气预报&a…...
锐评 Nodejs 设计模式 - 创建与结构型
本系列文章的思想,都融入了 让 Java 再次伟大 这个全新设计的脚手架产品中,欢迎大家使用。 单例模式与模块系统 Node 的单例模式既特殊又简单——凡是从模块中导出的实例天生就是单例。 // database.js function Database(connect, account, password)…...

【RoadRunner】自动驾驶模拟3D场景构建 | 软件简介与视角控制
💯 欢迎光临清流君的博客小天地,这里是我分享技术与心得的温馨角落 💯 🔥 个人主页:【清流君】🔥 📚 系列专栏: 运动控制 | 决策规划 | 机器人数值优化 📚 🌟始终保持好奇心&…...

15分钟学Go 第4天:Go的基本语法
第4天:基本语法 在这一部分,将讨论Go语言的基本语法,了解其程序结构和基础语句。这将为我们后续的学习打下坚实的基础。 1. Go语言程序结构 Go语言程序的结构相对简单,主要包括: 包声明导入语句函数语句 1.1 包声…...

【Qt】Qt的介绍——Qt的概念、使用Qt Creator新建项目、运行Qt项目、纯代码方式、可视化操作、认识对象模型(对象树)
文章目录 Qt1. Qt的概念2. 使用Qt Creator新建项目3. 运行Qt项目3.1 纯代码方式实现3.2 可视化操作实现 4. 认识对象模型(对象树) Qt 1. Qt的概念 Qt 是一个跨平台的 C 图形用户界面应用程序开发框架。它是软件开发者提供的用于界面开发的程序框架&#…...

论文笔记:PTR: Prompt Tuning with Rules for Text Classification
Abstract 手动设计大量语言提示麻烦且易出错,而自动生成的提示,在非小样本场景下验证其有效性昂贵且耗时。因此,提示调优以处理多类别分类任务仍然具有挑战。为此,本文提出使用规则进行多类别文本分类提示调优(PTR&…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)
笔记整理:刘治强,浙江大学硕士生,研究方向为知识图谱表示学习,大语言模型 论文链接:http://arxiv.org/abs/2407.16127 发表会议:ISWC 2024 1. 动机 传统的知识图谱补全(KGC)模型通过…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...