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的非阻塞版本, 很容易就能破坏请求与保持原则。
  •         破坏不剥夺——》只需要将对方的锁释放掉, 就能破坏掉不剥夺条件。
  •         破坏循环与等待条件——》破坏掉这个后, 申请锁的时候按照顺序申请锁!一般加锁的顺序保持一致!

  ——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!    

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

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

相关文章

《量子之歌》

第一章&#xff1a;曙光 在2045年的未来&#xff0c;人工智能不再是科幻作品中的虚构&#xff0c;而是成为了日常生活的一部分。在这个时代&#xff0c;深度学习模型已经变得如此庞大和复杂&#xff0c;以至于即使是最快的超级计算机也需要数小时才能完成一次完整的训练。 陈欣…

大华智能云网关注册管理平台 SQL注入漏洞复现(CNVD-2024-38747)

0x01 产品简介 大华智能云网关注册管理平台是一款专为解决社会面视频资源接入问题而设计的高效、便捷的管理工具,平台凭借其高效接入、灵活部署、安全保障、兼容性和便捷管理等特点,成为了解决社会面视频资源接入问题的优选方案。该平台不仅提高了联网效率,降低了联网成本,…

【前端】制作一个自己的网页(4)

刚才我们完成了网页中标题与段落元素的学习。在实际开发时&#xff0c;一个网页通常会包含多个相同元素&#xff0c;比如多个标题与段落。 对于相同标签的元素&#xff0c;我们又该如何区分定位呢&#xff1f; 对多个相同的标签分类 比如右图设置了七个段落元素&#xff0c;它…

DS堆的实际应用(10)

文章目录 前言一、堆排序建堆排序 二、TopK问题原理实战创建一个有一万个数的文件读取文件并将前k个数据创建小堆用剩余的N-K个元素依次与堆顶元素来比较将前k个数据打印出来并关闭文件 测试 三、堆的相关习题总结 前言 学完了堆这个数据结构的概念和特性后&#xff0c;我们来看…

react18中实现简易增删改查useReducer搭配useContext的高级用法

useReducer和useContext前面有单独介绍过&#xff0c;上手不难&#xff0c;现在我们把这两个api结合起来使用&#xff0c;该怎么用&#xff1f;还是结合之前的简易增删改查的demo&#xff0c;熟悉vue的应该可以看出&#xff0c;useReducer类似于vuex&#xff0c;useContext类似…

wxml 模板语法-数据绑定

mustache 语法的应用场景&#xff1a; 动态绑定内容&#xff1a; 动态绑定属性&#xff1a; 三元运算&#xff1a; 算数运算&#xff1a;

MobileNet v3(相比于MobileNet v2)

概述&#xff1a; 更新Block(bneck) 使用NAS搜索参数 &#xff08;Neural Architecture Search&#xff09; 重新设计耗时层结构 更准确&#xff0c;更高效 以及表中数据展示 更新Block 1.加入SE模块 2.更新了激活函数 首先通过一个1*1的卷积层来进行一个升维处理&#…

深入了解vcruntime140.dll:为什么会出现vcruntime140.dll丢失及修复

“找不到vcruntime140.dll”或“vcruntime140.dll丢失”是什么情况&#xff1f;出现这样的情况有什么方法可以解决&#xff1f;今天这篇文章将和大家聊聊vcruntime140.dll丢失错误的详细解决办法&#xff0c;一步步教大家修复错误vcruntime140.dll文件。 一步步修复错误vcrunti…

[含文档+PPT+源码等]精品基于springboot实现的原生微信小程序酒店管理系统[包运行成功+永久免费答疑辅导]

基于Spring Boot实现的原生微信小程序酒店管理系统&#xff0c;其背景主要源于酒店行业的信息化需求和移动互联网技术的快速发展。以下是对该背景的具体阐述&#xff1a; 一、酒店行业的信息化需求 信息量剧增&#xff1a; 随着旅游业的蓬勃发展&#xff0c;酒店数量不断增加&…

AI金融攻防赛:金融场景凭证篡改检测(DataWhale组队学习)

引言 大家好&#xff0c;我是GISer Liu&#x1f601;&#xff0c;一名热爱AI技术的GIS开发者。本系列文章是我跟随DataWhale 2024年10月学习赛的AI金融攻防赛学习总结文档。本文主要讲解如何解决 金融场景凭证篡改检测的核心问题&#xff0c;以及解决思路和代码实现过程。希望…

【图解版】力扣第1题:两数之和

Golang代码实现 func twoSum(nums []int, target int) []int {m : make(map[int]int)for i : range nums {if _, ok : m[target - nums[i]]; ok {return []int{i, m[target - nums[i]]}} m[nums[i]] i}return nil }

Java语言-抽象类

目录 1.抽象类概念 2.抽象类语法 3.抽象类特性 4.抽象类作用 1.抽象类概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c; 如果 一个类中没有包含足够的信息来描绘一个具体…

本地群晖NAS安装phpMyAdmin管理MySQ数据库实战指南

文章目录 前言1. 安装MySQL2. 安装phpMyAdmin3. 修改User表4. 本地测试连接MySQL5. 安装cpolar内网穿透6. 配置MySQL公网访问地址7. 配置MySQL固定公网地址8. 配置phpMyAdmin公网地址9. 配置phpmyadmin固定公网地址 前言 本文主要介绍如何在群晖NAS安装MySQL与数据库管理软件p…

Excel:vba实现合并工作表(表头相同)

这个代码应该也适用于一些表头相同的工作表的汇总&#xff0c;只需要修改想要遍历的表&#xff0c;适用于处理大量表头相同的表的合并 这里的汇总合并表 total 是我事先创建的&#xff0c;我觉得比用vba代码创建要容易一下&#xff0c;如果不事先创建汇总表就用下面的代码&…

多态对象的存储方案小结

某个类型有几种不同的子类&#xff0c;Jackson中的JsonTypeInfo 和JsonSubTypes可以应对这种情形&#xff0c;但有点麻烦&#xff0c;并且name属性必须是字符串、必须用Jackson为基础的json工具类对json字符串和对象进行序列化和反序列化。用过一次这种方案后边就不想再用了。 …

[Linux] 逐层深入理解文件系统 (1)—— 进程操作文件

标题&#xff1a;[Linux] 文件系统 &#xff08;1&#xff09;—— 进程操作文件 个人主页水墨不写bug &#xff08;图片来源于网络&#xff09; 目录 一、进程与打开的文件 二、文件的系统调用与库函数的关系 1.系统调用open() 三、内存中的文件描述符表 四、缓冲区…

python实现录屏功能

python实现录屏功能 将生成的avi文件转为mp4格式后删掉avi文件 参考感谢&#xff1a;https://www.cnblogs.com/peachh/p/16549254.html import os import cv2 import time import threading import numpy as np from PIL import ImageGrab from pynput import keyboard from da…

前海湾地铁A出口临时路边免费停车点探寻

​ ​我们公司不少同事下班直接从桂湾地铁步行到前海湾地铁A出口&#xff0c;很多人也是一样&#xff0c;可能是上下班高峰期停车还不如步行快&#xff1f;由于这条路的很多高楼大厦还没建好&#xff0c;一路过来可以看到不少路边停车点。以前海湾地铁A出口为例&#xff0c;…

数学建模算法与应用 第5章 插值与拟合方法

目录 5.1 插值方法 Matlab代码示例&#xff1a;线性插值 Matlab代码示例&#xff1a;样条插值 5.2 曲线拟合的线性最小二乘法 Matlab代码示例&#xff1a;线性拟合 5.3 最小二乘优化与多项式拟合 Matlab代码示例&#xff1a;多项式拟合 5.4 曲线拟合与函数逼近 Matlab代…

基于SSM的个性化商铺系统【附源码】

基于SSM的个性化商铺系统 效果如下&#xff1a; 用户登录界面 app首页界面 商品信息界面 店铺信息界面 用户功能界面 我的订单界面 后台登录界面 管理员功能界面 用户管理界面 商家管理界面 店铺信息管理界面 商家功能界面 个人中心界面 研究背景 研究背景 科学技术日新月异…