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线程的互斥的知识。 讲解流程为先讲解锁的工作原理, 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西, 但博主会总结两条结论!!最后就是讲一下死锁。 那么, 废…...
2024-10-17 问AI: [AI面试题] 讨论 AI 的挑战和局限性
文心一言 AI(人工智能)虽然取得了显著的进步,但仍面临多方面的挑战和局限性。以下是对AI挑战和局限性的详细讨论: 一、数据质量与可靠性 数据质量:AI系统依赖于高质量的数据进行训练和学习。如果数据质量低劣或包含…...
go基础(一)
包声明引入包函数变量语句&表达式注释 package main//包声明import "fmt"//引入包 //函数 func main() {/* 这是我的第一个简单的程序 */fmt.Println("Hello, World!") }基础语法 标记 go程序可以由多个标记组成,可以是关键字࿰…...
python忽略warnings 的方法
我在训练深度学习模型的时候一直出现这样的警告,但是不影响运行: UserWarning: Failed to load image Python extension: [WinError 127] 找不到指定的程序。 warn(f"Failed to load image Python extension: {e}") 要避免在 Python 程序运…...

2024年底蓝奏云最新可用API接口列表 支持优享版 无需手动抓取cookie
Lanzou Pro V1 接口列表 API状态版本路由获取文件与目录✅^1.0.1/v1/getFilesAndDirectories?url{}&page{}获取目录✅^1.0.0/v1/getDirectory?url{}获取文件✅^1.0.1/v1/getFiles?url{}&page{}搜索文件✅^1.0.0/v1/searchFile?url{}&wd{}依Id解析✅^1.0.2/v1/…...

Linux常用命令详细解析(含完整命令演示过程)
目录 1. 目录结构介绍 2. Linux命令基础 2.1 命令和命令行 2.2 格式 3. 常用命令 3.1 产看目录命令——ls 3.2 通配符 3.3 改变工作目录命令——cd 3.4 查看当前路径命令——pwd 3.5 创建新的目录命令——mkdir 3.6 创建文件目录命令——touch 3.7 查看…...

《使用Gin框架构建分布式应用》阅读笔记:p101-p107
《用Gin框架构建分布式应用》学习第7天,p101-p107总结,总计7页。 一、技术总结 1.StatusBadRequest vs StatusInternalServerError 写代码的时候有一个问题,什么时候使用 StatusBadRequest(400错误),什么时候使用 StatusIntern…...

014集——c#实现打开、另存对话框(CAD—C#二次开发入门)
如下图所示,运行后实现如下功能: 打开对话框,选择一个文件,并获取文件名变量。 打开另存对话框,输入路径和文件名,获取另存文件名变量。 部分代码如下: public static void Ofd(this Database…...

全面升级:亚马逊测评环境方案的最新趋势与实践
在亚马逊测评领域深耕多年,见证了无数环境方案的更迭与演变,每一次变化都体现了国人不畏艰难、勇于创新的精神。面对平台的政策调整,总能找到相应的对策。那么,当前是否存在一套相对稳定且高效的技术方案呢?答案是肯定…...
Java中的异步编程模型
1.什么是异步编程? 异步编程是一种编程模式,允许程序在等待某些操作(例如文件I/O或网络请求)完成时,不必停下来等待,而是继续执行其他任务。当异步操作完成时,回调函数或任务调度器会处理结果&…...
opencv 按位操作
opencv位运算说明 按位与,按位或,按位非,按位异或 在 OpenCV 中,按位操作函数的接口一般包括两个或多个图像数组(矩阵)作为输入,常常还会有一个可选的掩码参数。下面我列出每个函数的具体接口…...
【Bug】STM32串口空闲中断接收不定长数据异常
Bug 使用标准库配置STM32F103C8T6的串口1开启接收中断和空闲中断,通过空闲中断来判断数据发送是否结束,收到数据后切换板载LED灯所接引脚电平,发现LED出现三种情况,熄灭、微亮、正常亮,但是LED灯所接的GPIO引脚为PC13…...

使用Radzen Blazor组件库开发的基于ABP框架炫酷UI主题
一、项目简介 使用过ABP框架的童鞋应该知道它也自带了一款免费的Blazor UI主题,它的页面是长这样的: 个人感觉不太美观,于是网上搜了很多Blazor开源组件库,发现有一款样式非常不错的组件库,名叫:Radzen&am…...

Java入门4——输入输出+实用的函数
在本篇博客,采用代码解释的方法,帮助大家熟悉Java的语法 一、输入和输出 在Java当中,我们一般有这样输入输出: import java.util.Scanner;public class javaSchool {public static void main(String[] args) {Scanner scanner …...

《当尼采哭泣》
这是一个相互救赎的故事。故事铺垫比较冗长,看到一半的时候一度看不下去。直到看到最后两章才最终感觉值得一看。很多表层现象,就像露出水面的冰山。解决表面的问题,需要深挖冰山水下的部分。一个人碰到的最难解决的问题不在外部,…...
TOMCAT Using CATALINA——OPTS,闪退解决方法(两种)
【Java实践】安装tomcat启动startup.bat出现闪退问题_安装tomcat点击startup闪退-CSDN博客...

Android音视频 MediaCodec框架-启动编码(4)
Android音视频 MediaCodec框架-启动编码 简述 上一节我们介绍了MediaCodec框架创建编码器流程,编解码的流程其实基本是一样的,只是底层的最终的实现组件不同,所以我们只看启动编码流程。 MediaCodec启动编码 从MediaCodec的start方法开始…...
# Go 语言中的 Interface 和 Struct
go package mainimport ("fmt" )// Girl 是一个接口,定义了所有"女性"类型都应该实现的方法 type Girl interface {call()introduce() }// Wife 结构体代表妻子 type wife struct {name stringage intyearsWed int }// call 方法…...
SSM与Springboot是什么关系? -----区别与联系
SSM(Spring Spring MVC MyBatis)和 Spring Boot 都是基于 Spring 框架的技术栈,但它们在使用方式、配置复杂度以及设计理念上有所不同。下面是 SSM 和 Spring Boot 之间的关系及主要区别: SSM (Spring Spring MVC MyBatis) 定…...

MATLAB小波变换图像融合系统
二、应用背景及意义 本课题利用小波变换进行图像的融合,然后对融合的结果进行图像质量的评价。所谓小波变换图像融合就是对多个的信息目标进行一系列的图像提取和合成,进而可以获得对同一个信息目标的更为精确、全面、可靠的高低频图像信息描述。并且也…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...

TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...