【Linux】从零开始认识多线程 --- 线程互斥

在这里插入图片描述

人生有许多事情
正如船后的波纹
总要过后才觉得美的
-- 余光中

线程互斥

  • 1 线程类的封装
    • 1.1 框架搭建
    • 1.2 线程启动
    • 1.3 线程终止
    • 1.4 线程等待
    • 1.5 运行测试
  • 2 线程互斥
    • 2.1 多线程访问的问题
    • 2.2 解决办法 --- 锁
    • 2.3 从原理角度理解锁
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 线程类的封装

学习线程互斥之前,我们先对linux的线程库进行封装,熟悉一下C++的线程库。并且方便我们后续使用

1.1 框架搭建

我们主要要实现start stop join三个功能,线程启动,线程终止,线程等待。完成这些就可以快速使用线程了!

类内部需要:

  1. 线程名字:name
  2. 线程ID :进行等待和终止关键
  3. 是否运行判断 :只有运行状态才可以进行终止和等待
  4. 线程需要执行的回调函数指针 typedef void(*func_t)(const std::string& name)
  5. 函数返回值 void* result

拥有这些成员变量,就这样就可以保证我们的基本工作了!

namespace ThreadMouble
{//回调函数的类型typedef void(*func_t)(const std::string& name);class Thread{public://构造函数需要传入名字和回调函数Thread(const std::string& name , func_t func):_name(name), _func(func){}bool Start(){}void Stop(){}void Join(){}~Thread(){}private://线程名字std::string _name;//线程IDpthread_t id;//是否运行判断符bool isrunning;//回调函数func_t _func;//函数返回值std::string _result;};
}

1.2 线程启动

线程启动接口很简单就可以实现,我们调用系统调用pthread_create传入对应的参数.
但是执行的ThreadRun函数就要费一些头脑,pthread_create系统调用中需要传入一个void* (* )(void*)的函数指针

   void* ThreadRoutinue(void* args) --- 执行回调方法

但是对象内的函数都有一个默认参数 this指针,所以需要加入 static修饰成为类的函数,这样也造成不能调用内部的成员了, 为了优雅的执行 多加一个Excute()成员 进行调用回调函数

     	void Excute(){isrunning = true;_func(_name);isrunning = false;}static void* ThreadRun(void* args){//获取类对象Thread* self = static_cast<Thread*>(args);self->Excute();return nullptr;}bool Start(){//需要启动线程isrunning = true;int n = ::pthread_create(&id , nullptr , ThreadRun , this);if(n == 0){return true;}else{return false;}}

PS: ::表示使用标准库的接口
这样就优雅的执行了线程启动

1.3 线程终止

只有线程运行了才可以进行终止,直接调用系统调用即可

	void Stop(){if(isrunning ){isrunning = false;::pthread_cancel(id);}return ;}

1.4 线程等待

直接调用系统调用即可!

        void Join(){::pthread_join(id , nullptr);return ;}

1.5 运行测试

我们写好了线程的封装,接下来就来使用一下,来看看效果:

#include<iostream>
#include"Thread.hpp"
#include<unistd.h>using namespace ThreadMouble;void threadrun(const std::string& name)
{while(true){std::cout << "name: " << name << " is running..." << std::endl;sleep(1);}return ;
}int main()
{Thread t("thread-1" , threadrun);t.Start();std::cout << t.status() << std::endl; sleep(10);t.Stop();t.Join();return 0;
}

运行来看:
在这里插入图片描述
很好,可以正常创建线程并执行任务!

2 线程互斥

线程可以看到的大部分资源是共享资源,即多个线程可以看到的资源叫做共享资源!那么如果今天这个共享资源是一个大数组,一个线程可以进行写入,其他线程可以进行读取,这样不就实现了线程通信了!可是还是有问题的,因为线程读取不受对方控制,可以刚写一个字符立马就被读取了。就造成了读取不一致的问题。所以共享资源往往需要进行保护,类似取ATM机取钱,虽然是公共场所但是只有你一个人可以使用当前的ATM机。而线程也有这样的场景,就是线程互斥!

2.1 多线程访问的问题

首先我们先来看看多线程访问中会遇到的问题 — 我们设置一个情景,抢10000张票,我们设置4个并发线程一起来抢票:

  1. 按理来说每个线程只会在有票的时候可以抢
  2. 抢到就总数减一 , 直到没票为止
#include <iostream>
#include "Thread.hpp"
#include <unistd.h>
#include <vector>using namespace ThreadMouble;// 一共10000张票
int num = 10000;void threadrun(const std::string &name)
{while (true){if (num > 0){usleep(1000);//抢票的时间std::cout << "name: " << name << "剩余票数: " << num << std::endl;num--;}else{break;}}return;
}int main()
{std::vector<Thread> thds;for (int i = 0; i < 4; i++){char buffer[128];std::string name = "thread-" + std::to_string(i);snprintf(buffer, 128, "%s", name.c_str());thds.emplace_back(buffer, threadrun);}for (int i = 0; i < 4; i++){thds[i].Start();}std::cout << "所以票已经强光!!! " << std::endl;for (int i = 0; i < 4; i++){thds[i].Join();}return 0;
}

我们运行看看:
在这里插入图片描述
运行之后发现怎么抢到了负数票?这是为什么???这其实就是多线程并发访问中会遇到的问题,访问全局资源时就发生了问题!
我们分析一下为什么会发生问题

直接原因
  • 判读的过程其实是一种计算,计算结果是真和假(逻辑运算),是由CPU进行的,当CPU进行计算时,会先在内存中读取num ,然后再到一个寄存器中进行储存,再然后将判断数0移动到寄存器进行判断,最后得到结果。每个线程都会进行这样一个过程
  • 一行代码可能对应多条汇编指令,执行汇编指令 中随时都可能切换到其他线程
  • CPU中只有一套寄存器,但是寄存器的数据可以有多套(属于线程私有,看起来放在一套公共资源中,它会带走自己的数据,回来时恢复),所以假如在判断过程中进行了线程的切换,此时还没有进行--,就可能造成多个线程都存储着最后一张票,这样就造成了负数!

2.2 解决办法 — 锁

为了解决上述的问题,就要使用锁,我们先来了解锁和对应接口。
在这里插入图片描述
在pthread库中有我们锁的对应接口,和类型pthread_mutex_t互斥锁(任何时刻只允许一个线程进行资源访问)。有了这把锁既有对应的初始化和销毁。设置时不管是全局的还是静态的,只需要进行init即可。

  1. pthread_mutex_init的第一个参数传入锁的地址,第二个参数设置为nullptr就行.
  2. pthread_mutex_destory传入锁的地址就可以进行销毁了,全局或者静态的其实不需要进行主动销毁,在程序运行结束之后就自动销毁了。使用临时锁时才需要进行主动销毁

在这里插入图片描述

进行加锁时需要使用lock ,解锁使用unlock,非常直观!在使用过程中,会有多个线程竞争一个锁,成功的正常运行,失败的直接阻塞。

所谓的对共享资源的保护,本质是对临界区代码的保护!因为访问资源是由代码进行访问的,把访问资源的代码保护起来就保护了共享资源!接下来我们来了解一下临界区和非临界区

在这里插入图片描述
在需要保护的区域进行上锁,使其串行执行线程,就不会出现之前并发执行的问题了!

我们快速上手一下:

void threadrun(const std::string &name)
{while (true){pthread_mutex_lock(&gmutex);if (num > 0){usleep(1000);std::cout << "name: " << name << "剩余票数: " << num << std::endl;num--;pthread_mutex_unlock(&gmutex);}else{pthread_mutex_unlock(&gmutex);break;}}return;
}

我们分析过,出现问题的原因是这个判断语句,也就是临界区,要在临界区之前上锁。也就是在进行抢票判断之前,我们先将代码上锁。之后处理完成就解锁(一定要保证解锁)。
在这里插入图片描述

注意:

  1. 加锁的粒度一定要小,只将临界区代码加锁就可以
  2. 任何进程要进行抢票,都得先申请锁,不应该有例外!
  3. 所有线程申请锁的前提是都要看到这把锁, 所以锁也是一种共享资源!是共享资源就有可能会出现之前的问题,但是锁用谁来保护呢?想要不发生问题就得保证加锁的过程必须是原子的(只有一条汇编指令)
  4. 原子性:要么不做,要做就做完,没有中间状态!
  5. 如果线程申请锁失败了,当前线程就会别阻塞!
  6. 如果线程申请锁成功了,当前线程就继续进行!
  7. 线程申请锁成功了,运行临界区的代码时,会进行切换吗?可以!加锁不会影响调度算法,只会影响线程会不会继续向下运行!加锁的线程可以放心运行,不会受到打扰!

总之,对于其他线程,要么没有申请锁,要么释放了锁,对于其他线程才有意义!相当于我访问临界区,对于其他线程是原子的!

我们在对锁和线程名进行一个整体封装,更加优雅地进行使用:

// 包含回调函数所需的数据class ThreadData{public:ThreadData(const std::string name, pthread_mutex_t *gmutex) : _name(name), _lock(gmutex){}~ThreadData(){}public:std::string _name;pthread_mutex_t *_lock;};

再稍微修改一下线程类内部的构造函数,将主函数的传参修改一下:

int main()
{//设置一个局部锁pthread_mutex_t mutex ;pthread_mutex_init(&mutex , nullptr);std::vector<Thread> thds;for (int i = 0; i < 4; i++){char buffer[128];std::string name = "thread-" + std::to_string(i);snprintf(buffer, 128, "%s", name.c_str());//每个线程都需要一个td对象ThreadData *td = new ThreadData(name, &mutex);thds.emplace_back(name, threadrun, td);}for (int i = 0; i < 4; i++){thds[i].Start();}for (int i = 0; i < 4; i++){thds[i].Join();}//销毁锁pthread_mutex_destroy(&mutex);return 0;
}

我们运行一下:
在这里插入图片描述
可以看到使用的是同一个锁!

我们还可以进行优化,我们可以将锁单独封装起来,做到自动解锁释放:

#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *td) : _td(td){pthread_mutex_lock(_td);}~LockGuard(){pthread_mutex_unlock(_td);}private:pthread_mutex_t *_td;
};

这样每次在临界区之前创建一个锁对象,就可以完成对临界区的保护!

2.3 从原理角度理解锁

上面我们见到了锁的作用,那我们如何理解:

  1. 申请锁成功,允许进入临界区
  2. 申请锁失败,不允许进入临界区
    很简单,申请成功了,函数pthread_mutex_lock()会返回,否则不返回(就阻塞了,直到函数内部被唤醒,重新申请锁)!这就一个类似scanf()的情况 。
  • 我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,因为++这个运算至少经历三条汇编语句,在运行其中一条时退出, 有可能会有数据一致性问题!
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性(一条汇编语句的是原子性的)。即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

我们可以画图来看:

  1. 首先先确认一个概念:lock为1才能进行。
  2. 将内存中锁数据与寄存器的数据交换,将 1 移动到寄存器中,而寄存器此时数据是属于当前线程的,在线程调度完之后,就会将这个1带走!
  3. 下一个线程进行锁数据与寄存器的数据交换,只会得到0就阻塞在这里了
  4. 等锁住的线程执行结束,进行解锁,将1交换到内存中,此时就可以别其他线程使用了!

在这里插入图片描述
后序文章继续学习线程互斥与线程同步!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

Java语言程序设计——篇七(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 继承 类的继承实战演练 方法覆盖实战演练 &#x1f351;super关键字实战演练 调用父类的构造方法 类的继承 通过类的继承方式&#xff0c;可以…

手机图片如何转化为word文档?分享3种好用的软件。

在数字化时代&#xff0c;手机已经成为我们生活中不可或缺的一部分。随着手机拍照功能的日益强大&#xff0c;我们常常用手机记录下重要的信息和瞬间。但你有没有遇到过这样的烦恼&#xff1a;如何将手机里的图片快速转化为可编辑的Word文档呢&#xff1f;今天&#xff0c;就为…

基于 G6 的交互式过滤镜:探索图谱数据的新视角

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 基于 G6 的交互式过滤镜&#xff1a;探索图谱数据的新视角 应用场景 交互式过滤镜是一种强大的工具&#xff0c;它允许用户通过聚焦于图谱中的特定区域来探索和分析数据。它在各种场景中都有应用&#xff0c;…

马来西亚外贸服务器租赁公网带宽费用和测速IP地址性能测试

云服务器马来西亚&#xff08;吉隆坡&#xff09;公网带宽租用费用&#xff0c;马来西亚地域按固定带宽计费1M价格22元1个月、按使用流量计费1GB流量费用是0.529元&#xff0c;马来西亚服务器测试IP地址速度如何&#xff1f;阿里云服务器网aliyunfuwuqi.com整理2024年最新马来西…

AS-V1000系统主要功能介绍:实现视频监控统一接入汇聚

目录 一、系统概述 1、平台简介 2、视频监控统一接入能力 3、功能介绍 二、功能说明 1. 视频监控统一接入汇聚 2. 视频存储、回放和堆叠 3. 实时监控与预警、定位 4. 信息共享与联动、分发 5. 远程监控、管理和控制 三、主要特点 1. 多协议多品牌支持 2. 大容量集…

MQ消息队列+Lua 脚本实现异步处理下单流程,将同步下单改为异步下单

回顾一下下单流程&#xff1a; 用户发起请求 会先请求Nginx,Nginx反向代理到Tomcat&#xff0c;而Tomcat中的程序&#xff0c;会进行串行工作&#xff0c; 分为以下几个操作&#xff1a; 1 查询优惠券 2 判断秒杀库存是否足够 3 查询订单 4 校验是否是一人一单 5 扣减库…

QT信号和信号槽

信号和信号槽 一.信号与槽1.信号和槽的概述1.2.信号的本质1.3.信号的本质 二.信号和槽的使用2.1 连接信号和槽connect()函数原型&#xff1a;参数的说明 三.自定义信号和槽3.1基本语法1.自定义信号槽的书写规范2、自定义槽函数书写规范3.发送信号 3.2带参数的信号和槽 四.信号与…

在VMware16版本中安装ubuntu22.04.4镜像以及ubuntu镜像文件下载,配置更改,安装常用软件

目录 一、Ubuntu镜像文件下载 二、Ubuntu安装过程 三、更换国内镜像 四、安装常用软件 1、编译工具 2、代码管理工具 一、Ubuntu镜像文件下载 1-1、官网https://ubuntu.com/download 1-2、镜像网站快速下载 官网下载速度慢的话可以直接百度各大学的镜像下载网站去下载&…

multiprocessing.Pool创建多进程,导致内存不断攀升的解决方法

问题 使用multiprocessing.Pool创建多进程时&#xff0c;每个进程占用内存不断攀升。 问题描述 原本每个子进程没有占用那么多内存&#xff1a; 第二次读取新一批数据&#xff0c;每个子进程都复制了之前的内存资源&#xff1a; 原因说明 实际上&#xff0c;multiprocessing…

【高可用】利用AOP实现数据库读写分离

最近项目中需要做【高可用】数据库读写分离相关的需求&#xff0c;特地整理了下关于读写分离的相关知识。项目中采用4台数据库&#xff1a;1个master&#xff0c;2个slave&#xff0c;1个readOnly&#xff0c;其中master数据库会自动定时同步到readOnly节点。可以通过中间件(Sh…

FastAPI(六十九)实战开发《在线课程学习系统》接口开发--修改密码

源码见&#xff1a;"fastapi_study_road-learning_system_online_courses: fastapi框架实战之--在线课程学习系统" 之前我们分享了FastAPI&#xff08;六十八&#xff09;实战开发《在线课程学习系统》接口开发--用户 个人信息接口开发。这次我们去分享实战开发《在线…

Redis集群的主从复制原理-全量复制和增量复制-哨兵机制

Redis集群的主从复制原理-全量复制和增量复制-哨兵机制 作用 数据备份 这一点直观,因为现在有很多节点,每个节点都保存了原始数据的备份. 读写分离 这一点主要是当发生读写的时候&#xff0c;读数据的操作大部分都会进入到从节点&#xff0c;而写数据的操作都会进入到主节点&…

ESP32CAM人工智能教学15

ESP32CAM人工智能教学15 Flask服务器TCP连接 小智利用Flask在计算机中创建一个虚拟的网页服务器服务器&#xff0c;让ESP32Cam通过WiFi连接&#xff0c;把摄像头拍摄到的图片发送到电脑中&#xff0c;并在电脑中保存成图片文件。 Flask是用Python编写的网页服务程序WebServer。…

逻辑回归推导

逻辑回归既可以看作是回归算法&#xff0c;也可以看做是分类算法。通常作为分类算法使用&#xff0c;只可以解决二分类问题。 在上述平面中&#xff0c;每个颜色代表一个类别&#xff0c;即有4个类别 将红色的做为一个类别&#xff0c;其他三个类别都统称为其他类别&#xff0…

现代化电商企业在行业竞争中关于数据采集API接口的应用分析||经验分享

及时准确&#xff1a;电商API接口能为品牌提供实时数据&#xff0c;这意味着企业可以即时获取最新的商品价格信息&#xff0c;避免因为信息延迟导致的决策失误。相较于手动采集&#xff0c;接口数据一般更为准确可靠。 效率提升&#xff1a;接口自动化采集大大提高了数据获取效…

Photoshop(PS) 抠图简单教程

目录 快速选择 魔棒 钢笔 橡皮擦 蒙版 通道 小结 可以发现&#xff0c;ps逐渐成为必备基础的办公软件。本文让ps新手轻松学会抠图。 快速选择 在抠图之前&#xff0c;先了解下选区的概念。ps中大多数的抠图操作都是基于选区的&#xff0c;先选区再Ctrl J提取选区。而快…

【深度】2024AI大模型算力芯片产业深度分析

人工智能算力基础设施成为我国数字经济高质量发展的重要战略部署&#xff0c;具有重大发展意义。 1&#xff09;算力普适普惠化是大趋势&#xff0c;相关服务生态逐步构建。“东数西算”工程的实施&#xff0c;带动数据、算力跨域流动&#xff0c;实现产业跃升和区域平衡发展。…

谷粒商城实战笔记-46-商品服务-API-三级分类-配置网关路由与路径重写

文章目录 一&#xff0c;准备工作1&#xff0c;新增一级菜单2&#xff0c;新增二级菜单 二&#xff0c;前端树形界面开发1&#xff0c;开发分类展示组件 三&#xff0c;远程调用接口获取商品分类数据1&#xff0c;远程调用2&#xff0c;路由配置 错误记录 本节的主要内容&#…

【算法/训练】:动态规划

一、路径类 1. 字母收集 思路&#xff1a; 1、预处理 对输入的字符矩阵我们按照要求将其转换为数字分数&#xff0c;由于只能往下和往右走&#xff0c;因此走到&#xff08;i&#xff0c;j&#xff09;的位置要就是从&#xff08;i - 1&#xff0c; j&#xff09;往下走&#x…

MySQL 约束 (constraint)

文章目录 约束&#xff08;constraint)列级约束和表级约束给约束起名字&#xff08;constraint)非空约束&#xff08;no null)检查约束&#xff08;check)唯一性约束 (unique)主键约束 (primary key)主键分类单一主键复合主键主键自增 &#xff08;auto_increment) 外键约束外什…