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

[Linux#42][线程] 锁的接口 | 原理 | 封装与运用 | 线程安全

 互斥量 mutex

• 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间 内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通 过数据的共享,完成线程之间的交互。

• 多个线程并发的操作共享变量,会带来一些问题。

例如想进 单人自习室,要拿钥匙,申请的是同一把锁

1. 锁的接口

定义锁

锁也是一个数据类型,它的类型是pthread_mutex_t

初始化

  1. 静态分配
  • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER(不需要销毁)
  1. 动态分配
  • 函数原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  • mutex:要初始化的互斥量
  • attr:初始化互斥量的属性,一般设置为nullptr即可

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

对于锁的五个基本使用:

  • 注意:加锁的时候,一定要保证加锁的粒度,越小越好

实操:

//定义pthread_mutex_t mtx;
//初始化
pthread_mutex_init(&mtx, nullptr);
//销毁
pthread_mutex_destroy(&mtx);
//给线程上锁int n = pthread_mutex_lock(td->_pmtx);
//解锁n = pthread_mutex_unlock(td->_pmtx);

示例场景: 

#define THREAD_NUM 5int tickets = 10000;class ThreadData
{
public:ThreadData(const string &name, pthread_mutex_t *pm): _name(name), _pmtx(pm){}
public:string _name;pthread_mutex_t *_pmtx;
};void *GetTickets(void *args)
{ThreadData *td = (ThreadData *)args;while(true){int n = pthread_mutex_lock(td->_pmtx);assert(n == 0);if(tickets > 0){usleep(1000);printf("%s:%d\n", td->_name.c_str(), tickets);tickets--;n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);}else{n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);break;}}delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);// 多线程抢票逻辑pthread_t t[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);pthread_create(t + i, nullptr, GetTickets, (void *)td);}for (int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);return 0;
}
  • 加锁的本质:是用时间来换取安全
  • 加锁的表现:线程对于临界区代码串行执行
  • 加锁原则:尽量的要保证临界区代码,越少越好

加锁和解锁之间称之为 临界区

 int n = pthread_mutex_lock(td->_pmtx);assert(n == 0);if(tickets > 0){usleep(1000);printf("%s:%d\n", td->_name.c_str(), tickets);tickets--;n = pthread_mutex_unlock(td->_pmtx);

申请锁成功了,才能往后执行,不成功,阻塞等待

  • 线程对于锁的竞争能力可能会不同, 刚执行完,离锁近,所以可能会某一线程一直更容易拿到锁
  • 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
  • 纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥

观察员:

  • 外面来的,必须排队
  • 出来的人,并不能立马重新申请锁,必须排队到队尾

让所有的线程(人 )获取锁,按照一定的顺序。

按照一定的顺序获取资源--同步!

  • 锁本身就是共享资源
  • 所以,申请锁和释放锁本身就是原子的

临界区中,线程可以被切换吗?可以切换

  • 在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有人能进入临界区访问临界资源
  • 对于其他线程来讲,一个线程要么么有锁,要么释放锁
  • 当前线程访问临界区的过程,对于其他线程是原子的

💡引入新的解决方案,也会伴随着新的问题,在于看重什么,对立与统一 锁添加的智慧


2. 锁的原理

加锁

tickets-- 不是原子的?会变成 3 条汇编语句。原子:一条汇编语句就是原子的

  • 为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令 的作用是把存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使 是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另 一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改 一下

三部分的刷入,刷出,制作动图如下

(线程加载到寄存器,与内存实现交换后,带着数据及上下文游走)

交换的本质:把内存中的数据(共享),交换到 CPU 的寄存器中,其实是换到 CPU 此时执行的线程的硬件上下文中,数字 1 (锁)只有一个,随着上下文走了

把一个共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中--当前线程持有锁了

解锁

将 1 还回去,通过 unlock

💡许多奇奇怪怪的问题,是需要程序员你自己来维护的,这就设计到加锁位置的智慧了


3. 锁的应用--封装

锁的设置:(降低耦合度

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_;
};

封装:利用初始化来对锁进行启动

class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):mutex_(lock){mutex_.Lock();}~LockGuard(){mutex_.Unlock();}
private:Mutex mutex_;
};

调用:

#include "LockGuard.hpp"
while (true){{LockGuard lockguard(&lock); // 临时的LockGuard对象, RAII风格的锁!if (tickets > 0){usleep(1000);printf("who=%s, get a ticket: %d\n", name, tickets); // ?tickets--;}elsebreak;}usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟}

批注:

  1. LockGuard lockguard(&lock); // 临时的LockGuard对象, RAII风格的锁!

while 之后会自动解锁,利用了对象的生命周期

  1. 第二个{ ,明确出了锁的临界区

4. 可重入 VS 线程安全

概念

  • 线程安全--多线程的并发:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变 量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入--函数的特点同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其 他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会 出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

可重入的一般也是线程安全的

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况

• 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般 来说这些线程是安全的

• 类或者接口对于线程来说都是原子操作

• 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

• 调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的

• 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构,例如 STL

• 可重入函数体内使用了静态的数据结构

常见可重入的情况

• 不使用全局变量或静态变量

• 不使用用 malloc 或者 new 开辟出的空间

• 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

• 函数是可重入的,那就是线程安全的

• 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

• 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

• 可重入函数是线程安全函数的一种

• 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

• 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入 函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁--代码不会再完后推进了,例如:

 while (true){pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。if(tickets > 0){usleep(1000);...}}

下篇文章将继续讲解~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 使用 Vue 官方脚手架初始化 Vue3 项目
  • 基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(二)---ROS2与UE5进行图像数据传输
  • 多维的vector也可以sort!力扣刷题-合并区间有感
  • Esxi 7.0 安装windows xp 问题汇总
  • 大模型面试问题记录
  • 2018年高教社杯全国大学生数学建模竞赛(ABCD题)题目及附件
  • 数据库分库分表的介绍
  • 浅谈如何克服编程学习中的挫折感
  • java版知识付费saas租户平台的核心功能设计:打造高效、个性化的学习体验
  • 在 Hub 上使用 Presidio 进行自动 PII 检测实验
  • 3154. 到达第 K 级台阶的方案数(24.8.20)
  • C++ | Leetcode C++题解之第343题整数拆分
  • 学分绩点预警系统设计与实现(源码+lw+部署文档+讲解等)
  • Java--SpringBoot工厂模式
  • R 语言学习教程,从入门到精通,R 数据重塑(15)
  • [iOS]Core Data浅析一 -- 启用Core Data
  • [js高手之路]搞清楚面向对象,必须要理解对象在创建过程中的内存表示
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • ES6核心特性
  • gulp 教程
  • iOS 颜色设置看我就够了
  • js写一个简单的选项卡
  • leetcode讲解--894. All Possible Full Binary Trees
  • PHP变量
  • Vim Clutch | 面向脚踏板编程……
  • vue-cli在webpack的配置文件探究
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 动态规划入门(以爬楼梯为例)
  • 码农张的Bug人生 - 初来乍到
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 前端每日实战:61# 视频演示如何用纯 CSS 创作一只咖啡壶
  • 实习面试笔记
  • 想写好前端,先练好内功
  • 一道闭包题引发的思考
  • Java性能优化之JVM GC(垃圾回收机制)
  • Linux权限管理(week1_day5)--技术流ken
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • $HTTP_POST_VARS['']和$_POST['']的区别
  • (2024,Vision-LSTM,ViL,xLSTM,ViT,ViM,双向扫描)xLSTM 作为通用视觉骨干
  • (CPU/GPU)粒子继承贴图颜色发射
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (附源码)ssm学生管理系统 毕业设计 141543
  • (没学懂,待填坑)【动态规划】数位动态规划
  • (限时免费)震惊!流落人间的haproxy宝典被找到了!一切玄妙尽在此处!
  • (转)jQuery 基础
  • (转)ORM
  • (转载)Google Chrome调试JS
  • ****** 二 ******、软设笔记【数据结构】-KMP算法、树、二叉树
  • . Flume面试题