【Linux】互斥量原理的实现

news2025/4/9 6:07:34

深刻理解互斥锁

文章目录

  • 前言
  • 一、demo版的线程封装
  • 二、demo版的锁封装
  • 总结


前言

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

 

 对于上图中的加锁解锁汇编代码,是谁在执行呢?答案是调用的线程。

 这里圈出来的汇编代码的意思是:将共享数据交换到自己的私有上下文当中。这是什么意思呢下面我们详细的讲解一下:

首先我们的线程进来后加锁然后向自己的上下文写入0就是上面图中这一局代码,在这里要记住未来我们切换线程的时候会将这个上下文中的0带走的,因为我们定义的锁在内存中存放,既然是内存那么注定了这个锁是共享的。然后我们执行下一条指令就是倒数第二张图中红色部分,这里就是将寄存器里的值和mutex变量里的内容做交换,以前我们的寄存器是0内存是1,现在变成了1,0如下图:

 这也就证明了我们刚刚说的将共享数据交换到自己的私有上下文当中 ,这就是加锁的原理。

这个时候进入if语句,因为我们刚刚和寄存器做了交换,所以这次直接加锁成功返回0.在这里我们提一句,如果还没进入if判断语句线程就被切换了会怎么样呢?这个时候第一个线程会带走自己的上下文,也就是说寄存器里的1没有了被线程带走了,如下图:

 这个时候第二个线程进来了,第二个进程也要申请锁,所以先和内存中的mutex做交换,因为刚刚mutex的1已经被第一个线程拿走了,所以交换完寄存器和mutex还是0,这个时候进入if判断语句发现不大于0只能挂起等待,后续来申请锁的线程都是如此,因为1(钥匙)已经被第一个线程拿走了!!这个时候操作系统将第一个线程拿过来,然后发现寄存器中的内容1大于0然后就申请锁成功了。所以上面的mutex的1只能进行流转,不会新增任何的1。解锁也很简单,直接将执行流中的mutex改为1即可,因为解锁只有一条指令,所以相当于原子性的解锁了。 


一、demo版的线程封装

class Thread
{
public:
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);
private:
     pthread_t _tid;
     std::string _name;
     void*_args;
     func_t _func;     //线程未来要执行的回调
     ThreadStatus _status;
};

首先我们把线程内部的内容写出来,线程中需要有线程的状态,和函数指针完成回调函数,以及线程的id,名称,可变参数列表等,我们对于函数指针的设计完全是和库里面的一样的,下面我们把需要的函数写出来:

Thread(int num, func_t func, void* args)
     :_tid(0)
     ,_status(NEW)
     ,_func(func)
     ,_args(args)
    {
        char name[128];
        snprintf(name,sizeof(name),"thread-%d",num);
        _name = name;
    }

对于线程内部的初始化我们直接将线程id初始化为0(注意一旦我们创建新线程后新线程的id会返回给tid),状态为new,然后将外面的函数指针传过来还有可变参数列表,在函数体内完成线程名称的打印。

int status() 
    { 
        return _status;
    }
    std::string threadname()
    {
        return _name;
    }

线程状态和线程名称都可以直接返回给用户,下面我们实现一下run接口:

void run()
    {
        int n = pthread_create(&_tid,nullptr,runHelper,this);
        if (n!=0)
        {
            exit(1);
        }
        _status = RUNNING;
    }

run接口就是创建线程了,我们这里的参数就是线程内部的tid,创建成功后tid会变成新线程的id,如果没有创建成功就退出,让其执行run函数,执行run函数还需要传我们的线程对象,因为下面的run函数是static类型无法访问类内私有成员。我们还需要将状态设为运行。

 static void *runHelper(void* args)   //static后无this指针,满足create接口的第三个参数的要求
    {
        Thread* ts = (Thread*)args;
        (*ts)();
        return nullptr;
    }
    void operator ()()
    {
        _func(_args);
    }

 run函数为了完成回调工作首先必须是static函数,因为类内成员函数会默认多一个参数,这个参数是this指针,而我们的回调函数只有一个参数是void*的,所以用static,然后我们将args强转为thread*,下面实现一个仿函数,仿函数是可以直接调用func函数,所以我们的线程对象使用()就会调用func函数。

void join()
    {
        int n = pthread_join(_tid,nullptr);
        if (n!=0)
        {
            std::cerr<<"main thread join thread"<<_name<<"error"<<std::endl;
            return ;
        }
        _status = EXITED;
    }

等待线程也很简单,如果等待不成功就打印错误码,将状态改为退出状态。

pthread_t threadid()
    {
        if (_status==RUNNING)
        {
            return _tid;
        }
        else 
        {
            std::cout<<"thread is not running,no tid"<<std::endl;
            return 0;
        }
    }

返回线程id前需要先判断该线程是否是运行状态,只有运行状态我们才返回其id值,否则就打印错误。

下面我们测试一下我们的线程:

#include "mythread.hpp"
#include <unistd.h>
using namespace std;

void threadRun(void* args)
{
    std::string message = static_cast<const char*>(args);
    while (true)
    {
        cout<<"我是一个线程,"<<message<<endl;
        sleep(1);
    }
}
int main()
{
    Thread t1(1,threadRun,(void*)"hello world");
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    t1.run();
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    t1.join();
    cout<<"thread name: "<<t1.threadname()<<"thread id:"<<t1.threadid()<<"thread status: "<<t1.status()<<endl;
    return 0;
}

 通过运行我们可以看到封装的线程并没有问题,下面我们把锁也自己封装一下然后用我们自己的的线程试一下

二、demo版的锁的封装

#include <iostream>
#include <pthread.h>

class Mutex    //自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex)
       :_pmutex(mutex)
    {

    }
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {

    }
private:
    pthread_mutex_t *_pmutex;
};

对于锁的封装就非常简单了,首先有一个锁的指针,初始化的时候把外部那个锁传给我们的指针,然后通过这个指针去进行加锁解锁操作。

class LockGuard   //自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex)
       :_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

然后我们在用一个类,里面有一个锁对象,当这个对象创建的时候会自动加锁,销毁的时候自动解锁,下面我们演示一下:

int main()
{
    Thread t1(1, threadRoutine, (void*)"hello world1");
    Thread t2(2, threadRoutine,(void*)"hello world2");
    Thread t3(3, threadRoutine,(void*)"hello world3");
    Thread t4(4, threadRoutine,(void*)"hello world4");
    t1.run();
    t2.run();
    t3.run();
    t4.run();

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}
int tickets = 1000; 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void* args)
{
    std::string message = static_cast<const char*>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex); //所有线程都要遵守这个规则
        if (tickets>0)
        {
            usleep(2000);  //模拟抢票花费的时间
            cout<<message<<" get a ticket: "<<tickets--<<endl;
            pthread_mutex_unlock(&mutex);
        }
        else 
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

 运行后抢票逻辑也是没有问题的,下面我们引入我们自己封装的锁:

 我们自己的锁只要在这个作用域就是加锁状态,出了作用域就销毁了用起来非常的方便:

 运行后和刚刚库里的锁一模一样。


总结

可重入 VS 线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况:
不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第10讲:深入剖析 Agent 插件原理,无侵入性埋点

AbstractClassEnhancePluginDefine 核心实现 在开始之前&#xff0c;先简单回顾上一课时中关于 AbstractClassEnhancePluginDefine 的一个核心知识点&#xff1a;AbstractClassEnhancePluginDefine 是所有插件的父类&#xff0c;SkywalkingAgent.Transformer 会通过其 enhance…

信号完整性:反射

反射是怎么形成的 信号的反射和互连线的阻抗密切相关。反射的最直接原因是互连线的阻抗发生了突然变化&#xff0c;只要互连线的阻抗不连续的点&#xff0c;该处就会发生反射。 信号是以电磁波的形式在走线中传播的&#xff0c;如果从传统的电路理论角度去看&#xff0c;是无…

YOLOv5使用自定义数据集实验

上一篇博文中介绍了YOLOv7训练自定义数据集&#xff0c;在这篇文章中&#xff0c;我们主要记录YOLOv5模型的实验过程&#xff0c;用于对比实验。 YOLOv5与YOLOv7毕竟一母同胞&#xff0c;因此部署起来也是极为类似。 数据集 数据集使用的与YOLOv7的实验数据集一样&#xff0c;…

windows服务器自带IIS搭建网站并发布公网访问

文章目录 1.前言2.Windows网页设置2.1 Windows IIS功能设置2.2 IIS网页访问测试 3. Cpolar内网穿透3.1 下载安装Cpolar3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5.结语 转载自远程源码文章&#xff1a;【IIS搭建网站】本地电脑做服务器搭建web站点并公网访问「内网…

VS2019生成和使用lib、dll文件

叠甲&#xff1a;本文非常简略&#xff0c;方法非常朴素&#xff0c;仅供参考。 目录 lib文件 生成lib文件 使用lib文件 dll文件 生成dll文件 使用dll文件 lib文件 生成lib文件 新建项目libTest。 右键项目→属性→配置属性→常规→配置类型&#xff0c;选择“静态库…

【Java】表白墙终章-飞流直下的“甜言蜜语”-瀑布流式布局

飞流直下三千尺&#xff01; 文章目录 【Java】表白墙终章-飞流直下的“甜言蜜语”-瀑布流式布局1. 效果前后对比2. 瀑布流式布局原理思想3. 约定前后端接口4. 后端代码4.1 修改Love类的定义4.2 修改doPost方法4.3 修改save方法4.4 修改doGet方法4.5 修改load方法 5. 前端瀑布流…

面试总结个人版

一、面试题 java 集合 &#xff0c; spring springmvc springboot springcloud 数据库相关的&#xff0c; redis 相关 &#xff0c;mq 相关 &#xff0c;结合业务的场景题 1、part one 集合 HashMap底层原理 HashMap是基于哈希表的Map接口的非同步实现。元素以键值对的形式存…

asp.net教师调课系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net教师调课管理系统 是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使用c#语言开发 asp.net教师调课系统VS开发sqlser…

deadline用WebService提交Job

官方文档 网站链接 进入rest API&#xff0c;点击jobs&#xff0c;找到submit job 这里可以看到消息体需要用到JobInfo和PluginInfo这两个关键的字典&#xff08;json object&#xff09; 拿到对应的键值对 为了填写url请求的消息体 我们需要拿到必须参数的键值对 点击双击…

如何延长电脑硬盘的使用寿命?

在日常使用电脑过程中&#xff0c;一定要做好硬盘的保养和维护&#xff0c;一旦硬盘损坏&#xff0c;保存在硬盘上的数据就会丢失&#xff0c;而且找回数据也是一件很费功夫的事情&#xff0c;甚至有可能永远也找不回来。所以日常工作中定期对资料进行备份&#xff0c;做好电脑…

【算法与数据结构】24、LeetCode两两交换链表中的节点

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;题目要求两两交换节点。在链表当中非常重要就是下一个节点&#xff0c;一旦丢失&#xff0c;这个节点后…

IDL基础语法

1 创建变量 命名规则&#xff1a;变量名必须以字母开头。它们可以包括其它字母&#xff0c;数字&#xff0c;下划线&#xff0c;美元符号。 以下是创建不同数据类型的方法&#xff0c;我们只需了解即可&#xff0c;知道如何创建整型【16位有符号长整型】和浮点型 PRO learn;创…

RedisGraph的整体架构

The architecture of RedisGraph 本文关注RedisGraph的整体架构&#xff0c;分别从图存储模型、索引、并发控制、和执行计划四个方面简要阐述。下图为RedisGraph的整体架构图。 1 图存储模型 了解一个图数据库的架构&#xff0c;最重要的就是其图存储模型&#xff0c;即其中的…

freeswitch 使用 silero-vad 静音拆分使用 fastasr 识别

silero-vad 在git 的评分挺高的测试好像比webrtc vad好下面测试下 silero-vad 支持c 和py 由于识别c的框架少下面使用py 以下基于python3.8torch1.12.0torchaudio 1.12.0 1.由于fastasr 需要16k 所以 将freeswitch的实时音频mediabug 8k转成16k 用socket传到py 模块代码…

二十三种设计模式(待更)

二十三种设计模式 二十三种设计模式结构型1.适配器 相关资料 二十三种设计模式 资料来源于老师讲解以及大佬的设计模式仓库 zhengqingya 结构型 将对象和类按某种布局组成更大的结构&#xff0c;并同时保持结构的灵活和⾼效。 1.适配器 将一个类的接口转换成客户希望的另外…

【小沐学Python】Python实现在线电子书(MkDocs + readthedocs + github + Markdown)

文章目录 1、简介2、安装3、创建新项目4、添加页面5、编辑导航页6、设置主题7、更改图标图标8、构建网站9、部署9.1 准备github项目9.2 注册登录Read the Docs9.3 导入github项目到 Read the Docs 10、Markdown语法10.1 横线10.2 标题10.3 段落10.4 文字高亮10.5 换行10.6 斜体…

你不可不知的八大全新顶级开源项目

导读九年来&#xff0c;Black Duck开源年度奖一直致力于发现过去一年中出现的最具创新性与影响力的开源项目。尽管开源项目阵营一直在快速变化&#xff0c;但年度新人奖一直在为行业趋势提供重要参考。下面&#xff0c;我们将了解这一年中的各位获奖新人! 九年来&#xff0c;B…

adb详细教程(一)-下载安装与环境变量配置

对于Android开发来说&#xff0c;adb是再熟悉不过的调试工具 但其实对于移动端的测试来说&#xff0c;adb也是一个十分重要的、能够提高测试工作效率的工具。 文章目录 一、介绍二、下载地址三、安装四、配置环境变量 一、介绍 全称 adb全称全称为Android Debug Bridge&#x…

【Python】在同一图形中的绘制多个子图

1. 引言 有时我们需要并排绘制两个图形&#xff0c;这不仅是为了更好地利用空间&#xff0c;而且主要是因为为了更加直观地对比分析数据。其实在python中可以利用subplot来实现上述功能。 闲话少说&#xff0c;我们直接开始吧&#xff01; 2. 准备工作 这里&#xff0c;我们…

JavaScript 教程---互联网文档计划

学习目标&#xff1a; 每天记录一章笔记 学习内容&#xff1a; JavaScript 教程---互联网文档计划 笔记时间&#xff1a; 2023-6-5 --- 2023-6-11 学习产出&#xff1a; 1.入门篇 1、JavaScript 的核心语法包含部分 基本语法标准库宿主API 基本语法&#xff1a;比如操作符…