Linux——线程(3)

在上一篇博客中,我介绍了关于Linux系统中pthread库线程的接口使用以
及对于pthread库的理解。但是我们单单会使用多线程的接口还不够,因为
在使用多线程解决问题的时候,由于进程中的数据对于其中的线程来说大
多是共享的,这也势必会导致线程对资源的访问是不安全的,所以我们还
需要能够避免或解决在使用多线程时可能会出现的各种问题。我今天会介
绍其中的第一部分内容,那就是互斥。

线程的互斥

一小段代码

我在准备这篇博客之前,还简单的模拟实现出了C++中的线程大致是什么样的,我后面对线程的操作都会使用这个封装好的接口,代码如下:

#include <iostream>
#include <functional>
#include <pthread.h>
using namespace std;template <class T>
using func_t = function<void(T)>;template <class T>
class Thread
{
public:Thread(string name, func_t<T> func, T arg): _name(name), _tid(0), _isrunning(false), _func(func), _arg(arg){}static void *threadroutine(void *arg){((Thread *)arg)->_func(((Thread *)arg)->_arg);}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, (void *)this);if(n == 0){_isrunning = true;return true;}}void Join(){pthread_join(_tid, nullptr);}bool IsRunning(){return _isrunning;}private:string _name;pthread_t _tid;bool _isrunning;func_t<T> _func;T _arg;
};

这段代码其中需要注意一些小细节,首先就是需要对模板能够有一定的了解,然后就是可能会有人在类中的Start函数中调用pthread_create函数的时候传的参数为什么还要再封装一层threadroutine函数,并且threadroutine函数还是static属性的?
首先,我们使用自己写的Thread的时候,需要线程执行的函数就需要创建对象的时候传过去保存在对象所属的类中的成员变量中,而且pthread_create函数中要求传的函数指针类型是void*(threadroutine)(void)类型的。
但是使用过C++标准库的读者知道,线程对象初始化的时候可没有要求这个函数的参数和返回值都是void的,所以这一点需要我们自己封装一下。
使用者传过来的函数我们并不知道是什么样子的,这由使用者决定,使用者清楚,所以我们需要再封装一层函数那就是threadroutine作为pthread_create函数的参数,然后threadroutine函数中再调用使用者所传入的函数,所以这时候要访问类内成员,这里有两种方式一种就是将threadroutine函数作为Thread类的友元,还有就是作为类内成员函数,这里我选择后者,至于为什么是threadroutine函数是static属性的,那是因为类内的成员函数的参数中第一个参数始终都是this指针,所以实际上成员函数是有着两个参数的,也就是threadroutine函数的类型是void
(threadroutine)(Tread, void*)。所以我们需要static修饰这个函数,从而能够符合pthread_ctreate函数的参数要求。

1. 问题的提出

我先来写出一段代码来抛出问题:
在这里插入图片描述
其中mythread.hpp就是我封装好的线程库。
这段代码的意思就是创建好五个线程,让这五个线程都去执行byticket,然后这五个线程以并发的方式让全局变量g_ticket进行–,假如该变量小于0的话就跳出循环结束线程,然后线程被回收。现在来运行一下这个代码:
在这里插入图片描述
神奇的一幕就出现了,g_ticket这个变量,竟然小于0了,要知道我们的代码在主观认为是不可能出现这个变量小于0的,这显然程序是错误的,而这就是线程不安全了。

2. 概念的提出

要真正的理解并解决上面出现的问题需要一些储备知识。
我们看到上面的代码它应该不会出现让g_ticket变量小于零的情况,但是事实就是出现小于零的情况了,这种多线程下的共享资源数据出现错误的现象叫做数据不一致
而被保护的共享资源叫做临界资源
我在这篇博客(进程间通信中的简单认识信号量一小节中)中也介绍临界资源的概念,保护临界资源也就是保护访问临界资源的代码,而这段代码也叫做临界区
而保护共享资源的一个方式就是互斥,互斥的意思就是在同一时刻只能由一个执行流进入临界区访问临界资源。
我也曾经介绍过原子性的概念,原子性的大致意思就是,一个任务如果是原子的,那么它只会有两种状态,完成和没有完成,也就是在进行该任务期间该执行流是不可被切换的。
现在我们来认识一个观点:假如有一个变量a我们让这个a++,其中这个a++这行代码是原子的吗?
在这里插入图片描述
这里直接给出结果并做出解释,a++不是原子的,因为我们如果能看到这行代码的汇编代码的话就能得知了:
在这里插入图片描述
我们看到a++在汇编代码下,它是由三条指令构成的,下面我来解释一下这三条指令的大致意思:
第一句就是a变量会从内存中转移到寄存器eax中
然后第二句就是执行++
第三局就是再将eax中的内容转移到内存中。
我们可以发现在c++代码中一行就可以完成的任务,对于汇编而言需要三步。我们通常认为一条汇编指令就是原子的。那么a++这行代码明显不是原子的。
既然不是原子的,那么我们可以假设这么一个事件:
我们创建好两个线程(A和B),然后让这两个线程执行同一个函数,在这个函数中对一个全局变量(g_val)做++。
在理论上就可能会出现这样的情况:
在这里插入图片描述
这里我们普及一个小知识点,我们知道CPU中由运算器支持运算,但是它不仅仅支持算术运算(加减乘除),它也支持逻辑运算(比如逻辑或,逻辑与)。

3. 解释问题

现在我们来分析最开始的抢票代码为什么会出现负数:
我们创建五个线程,让这五个线程执行一个简单的抢票逻辑,当这个票的数目不大于0的时候,跳出循环。经过上面的理论介绍之后,有些人会以为,这段代码主要的问题出现在g_ticket++了,但是真的是这样吗?
再线程执行的函数中有两个地方是真正对g_ticket进行运算的,一个是g_ticket++,一个就是if语句中的对g_ticket的判断,前面说过逻辑运算也是运算。既然它也是运算的话,g_ticket也肯定需要先被转移到对应的寄存器中,然后再判断g_ticket是否大于0。其实出问题的地方是在这里。当有一个线程A刚要开始准备对g_ticket–时(此时g_ticket值为1),时间片到了,切换到另一个线程B,g_ticket此时值为1,大于零,进入第一个分支中,然后对g_ticket–,这样g_ticket值就为0了,过了一会又调度上了线程A,线程A现在的动作是对g_ticket–啊,此时一旦减了之后,g_ticket就小于零了,又因为我们的代码中有多个线程,所以出现这种现象可能不止在一个线程中,所以会出先我们的代码运行之后出现g_ticket小于零的情况。也就是数据不一致,线程安全出现了问题,针对这种问题,其中的解决方式就是互斥。

4. Linux中的互斥

a. 接口介绍

我们知道互斥就是在一个时刻,只允许一个执行流访问临界资源,这样就可以对临界资源做很好的保护。保护临界资源也就是保护临界区,也就是我们对使用临界资源的代码进行保护就可以了,在Linux中互斥的体现就是加锁。这个锁叫做mutex,也叫互斥量。
现在我们就来认识一下Linux中关于锁的接口:
在这里插入图片描述
互斥锁其实就是一个类型为pthread_mutex_t的变量,既然是变量就可以分为局部变量和全局变量,在这两个变量中,全局变量可以使用PTHREAD_MUTEX_INITIALIZER来初始化,而局部变量就需要使用上面的pthread_mutex_init来初始化。
而锁的销毁就需要使用pthread_mutex_destroy来销毁。
在这里插入图片描述
而对临界区的保护,就是给临界区进行上锁。上锁的接口就是pthread_mutex_lock。
但是上锁的前提是执行流能够申请到锁资源(上面的接口也就是申请锁资源的过程),如果申请到了,那么该执行流就会对该临界区上锁,以后就只能由该执行流执行临界区的代码。
而没有申请到锁资源的则会被挂起阻塞,知道锁资源可申请成功。
所以为了申请失败后不让执行流挂起阻塞,pthread_mutex_trylock可以实现这一功能。它会跳过临界区执行之后的代码。
而pthread_mutex_unlock就是解锁,释放锁资源。

b. 全局变量的锁

接下来我们来使用一下这些接口,首先先使用全局的锁:
在对临界区上锁之前,我们先要清楚代码中哪一部分是临界区:
在这里插入图片描述
我们可以看到整段代码中,只有这一段代码访问了临界资源,所以我们也是对这段代码上锁:
在这里插入图片描述
在这里插入图片描述

这样的话我们就完成了对临界区的保护,我们再次运行代码:
在这里插入图片描述
就不会出现负数的情况了。
现在呢,我们做一个小实验,来观察一下,上锁前后代码的运行效率如何:
我这里在代码的前后使用了clock来观察大致的用时是多少:
在这里插入图片描述

上锁后:
在这里插入图片描述
上锁前:
在这里插入图片描述
可以看到差距还是很大的,因为上锁之后当有一个执行流执行临界区代码时,就意味着其他执行流,要执行pthread_mutex_lock时会被阻塞。这就会导致线程的效率变低,但是这种情况也是没有办法避免的,除非多执行流不访问共享资源。

c. 注意点

通过上面全局的锁的使用之后,我给出关于锁的使用的建议:

	1. 尽量对少的代码块加锁2. 一般加锁,都是给临界区加锁

这里还有一些问题,在我们使用锁的过程中,g_mutex这个变量是全局的,那就意味着该进程下的所有线程都能访问到这个变量,那么此时这个锁不也是共享资源吗?为什么不需要对这个锁进行保护呢?
这里不难想到,其实关于关于锁资源的申请的过程它是原子的,是安全的。
还有问题,当一个执行流申请到了锁资源开始执行临界区代码的过程中,这个执行流有没有可能被切换呢?
如果脑子能转过弯来的话也不难想到,当然可以了啊。切换并不会出现线程不安全的问题。对于进程的临界区资源本来就是只对进程内部的执行流内部可见的,当切换其他进程中的执行流时,其他进程的执行流对该共享资源不可见,自然就不会访问到该资源了,而对于自己进程下的线程虽然对共享资源是可见的,但是由于锁的存在,它也执行不到临界区中的代码,也就不会发生线程不安全的问题了。所以当一个执行流执行临界区的代码时,该执行流是可以被切换的。
我们发现此时临界区一定意义上好像也具有了“原子性”。

d. 局部变量的锁

现在我们来看看局部变量的锁又怎么使用呢?
在这里插入图片描述
在这里插入图片描述
先在我们可以对这个局部变量的锁进行一下简单的封装,让它使用起来更简单一些:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
这样当我需要对临界区上锁时我就可以创建这么一个变量,创建好之后锁也就上上了。当该变量生命周期结束的时候则会自动解锁所以我们还应该规范一下该变量的作用域:
在这里插入图片描述
花括号的外面就可以是非临界区代码了。

e. 理解锁

上面已经介绍了在多执行流执行并发访问共享资源会出现不一致的情况下如何使用加锁来解决问题。但是上面我提出一个疑问,那就是锁也是共享资源,为什么不需要被保护呢?我说加锁的过程是原子的。现在我就来解释一下如何理解加锁的过程是原子的。
我们知道我们使用C/C++写好代码之后,使用编译器编译代码的时候会有将代码转化为汇编代码的过程,我也说过我们可以将汇编代码的一条指令理解为原子的。其实在大多数的体系结构中都提供了诸如swap、exchange这样的指令,它的作用是将内存中的数和寄存器中的数进行交换。
我们假如要进行这样的交换,首先可能会在内存中开辟一个临时空间,然后将内存中的数存入到临时空间,再将寄存器中的内容放入到内存中,再将临时内存中的内容放到寄存器中,完成寄存器内容和内存内容数值的交换,看起来这样的过程有很多步,但其实在汇编层面这确实只是一步。
为了方便下面的理解,我们可以将锁抽象成只是一个整型变量mutex,当这个变量值为1的时候,就代表这个锁可申请,如果是0就代表不可申请。
接下来我给出一段关于加锁的伪代码:
在这里插入图片描述
那关于这段代码怎么理解呢?
在这里插入图片描述
从这其中的过程中,只要在进行exchange交换时mutex的值为1,当执行完成这条指令之后,其实这个线程的锁就已经相当于上好了,所以上锁的过程关键只看这一步之后,属于线程的上下文中%al中的内容是1时,该线程就已将相当于申请到锁资源了。而这句指令又是原子的,所以上锁的过程也就是原子的。
我们现在也可以重新理解一下:上锁的本质其实就是,将共享的mutex资源换入到自己的寄存器中,成为线程独有的资源。
而关于解锁,其实就很简单了,因为解锁的执行首先就意味着该线程有锁,既然有锁,那么解锁是不是原子的也无所谓了,反正任意时刻只能由一个执行流上同一把锁,这里给出解锁的伪代码:
在这里插入图片描述
而关于我们加锁的原则就是:谁加的锁,谁来解。

5. 互斥导致的问题,同步的提出

经过上面的介绍,我们知道在多执行流并发访问共享资源的情况下可以通过对访问共享资源的代码也就是临界区加锁形成互斥的情况从而防止出现数据不一致的线程不安全问题。但是单单的互斥也会出现问题,在有些操作系统下,执行上述的抢票代码会出现只有一个执行流一直能申请到锁的资源,而其它执行流一直被阻塞,直到进程结束:
这种现象是可能出现的,比如其中的一个执行流的优先级比较高,每次调度时都是优先调度它,那么每次它都能够申请到锁资源,其他线程一直被阻塞,票都让该线程抢走了。
而其他一直被阻塞的线程,我们称这些线程为饥饿线程。这种问题也叫做饥饿问题。
而解决这种问题也很简单(想到很简单,但是理解同步不简单),那就是让线程的执行具有一定的顺序性,而这就是同步,关于同步的话题,我会在之后进行讲述。

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

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

相关文章

css3实现3D立方体旋转特效源码

源码介绍 CSS3自动旋转正方体3D特效是一款基于css3 keyframes属性制作的图片相册自动旋转立方体特效 效果展示 下载地址 css3实现3D立方体旋转特效代码

发那科数控机床FanucCNC(NCGuide)仿真模拟器配置和数据采集测试

开发日记3.12 此篇用于记录发那科数控机床(Fanuc CNC)采集程序开发中&#xff0c;用虚拟机做测试时&#xff0c;虚拟机的配置和使用以支持采集软件开发和测试。 配置虚拟机使用仿真软件 下载VMware15 「链接&#xff1a;https://pan.xunlei.com/s/VNsl9Gmb14ANBiiNlsT7vA2LA…

01 THU大模型之基础入门

1. NLP Basics Distributed Word Representation词表示 Word representation: a process that transform the symbols to the machine understandable meanings 1.1 How to represent the meaning so that the machine can understand Compute word similarity 计算词相似度 …

新品发布:广州大彩科技COF系列2.1寸480*480 IPS 串口屏发布!

一、产品介绍 该产品是一款2.1寸分辨率为 480480的医用级工业组态串口屏&#xff0c;拥有2.1寸IPS液晶屏&#xff0c;分辨率有480480&#xff08;实际显示为R240内切圆区域&#xff09;&#xff0c;支持电容触摸。采用COF超薄结构工艺设计&#xff0c;用户安装便捷灵活&#x…

信号处理--基于Fisher分数的通道选择的多通道脑电信号情绪识别

目录 背景 亮点 环境配置 数据 方法 结果 代码获取 参考文献 背景 基于脑电的情绪分析&#xff0c;目前是当前研究的一个主要方向和热点。 亮点 使用基于Fisher score的标准来筛选具有高判别意义的脑电通道&#xff1b; 使用基于特征选择的遗传算法实现特征的筛选&#xff0c;从…

什么是VR虚拟现实体验店|VR主题馆加盟|元宇宙文化旅游

VR虚拟现实体验店是一种提供虚拟现实技术体验的场所。在这样的店铺里&#xff0c;顾客可以通过专业的设备和技术&#xff0c;体验虚拟现实技术带来的沉浸式感觉。 通常&#xff0c;这些商店提供一系列VR体验&#xff0c;包括互动游戏、沉浸式模拟、虚拟旅游和其他VR内容。客户可…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的水下目标检测系统(深度学习模型+UI界面+训练数据集)

摘要&#xff1a;本研究详述了一种采用深度学习技术的水下目标检测系统&#xff0c;该系统集成了最新的YOLOv8算法&#xff0c;并与YOLOv7、YOLOv6、YOLOv5等早期算法进行了性能评估对比。该系统能够在各种媒介——包括图像、视频文件、实时视频流及批量文件中——准确地识别水…

pytorch激活函数

目录 1.激活函数由来2. 常见激活函数2.1 Sigmoid2.2 Tanh2.3 relu 1.激活函数由来 科学家对青蛙的神经元进行研究的时候发现&#xff0c;只有超过一定的阈值青蛙才会有反应&#xff0c;因此不能将多个输入做简单的加权平均&#xff0c;而需要一个阶梯函数也就是激活函数&#…

UNIAPP微信小程序中使用Base64编解码原理分析和算法实现

为何要加上UNIAPP及微信小程序&#xff0c;可能是想让检索的翻围更广把。&#x1f607; Base64的JS原生编解码在uni的JS引擎中并不能直接使用&#xff0c;因此需要手写一个原生的Base64编解码器。正好项目中遇到此问题&#xff0c;需要通过URLLink进行小程序跳转并携带Base64参…

在没有推出硬盘的情况下,重启mac电脑,外接移动硬盘无法加载显示?

一、mac磁盘工具显示未装载 1.打开终端&#xff0c;输入 diskutil list查看当前硬盘列表&#xff0c;大多数时候&#xff0c;可以解决。 二、使用命令行装载硬盘 执行上面命令后&#xff0c;仍不起作用&#xff0c;则手动挂载&#xff0c;在命令行输入如下内容&#xff1a; …

腾讯云和阿里云4核8G云服务器多少钱一年和1个月费用对比

4核8G云服务器多少钱一年&#xff1f;阿里云ECS服务器u1价格955.58元一年&#xff0c;腾讯云轻量4核8G12M带宽价格是646元15个月&#xff0c;阿腾云atengyun.com整理4核8G云服务器价格表&#xff0c;包括一年费用和1个月收费明细&#xff1a; 云服务器4核8G配置收费价格 阿里…

部署 LVS(nginx)+keepalived高可用负载均衡集群

目录 一、集群的概述 1、什么是集群 2、普通集群与负载均衡集群 2.1 普通集群&#xff08;Regular Cluster&#xff09; 2.2 负载均衡集群&#xff08;Load Balancing Cluster&#xff09; 2.3 高可用集群&#xff08;High Availability Cluster&#xff09; 2.4 区别 …

公网IP与私有IP及远程互联

1.公网有私有IP及NAT 公网IP是全球唯一的IP&#xff0c;通过公网IP&#xff0c;接入互联网的设备是可以访问你的设备。但是IPV4资源有限&#xff0c;一般ISP(Internet Service Provider)并不会为用户提供公网IP。所以家里的计算机在公司是没法直接使用windows远程桌面直接访问…

187基于matlab的弹道目标跟踪滤波方法

基于matlab的弹道目标跟踪滤波方法&#xff0c;扩展卡尔曼滤波&#xff08;extended Kalman filter, EKF&#xff09;、转换测量卡尔曼滤波&#xff08;conversion measurement Kalman filter, CMKF&#xff09;跟踪滤波&#xff0c;得到距离、方位角、俯仰角误差结果。程序已调…

webpack5零基础入门-5使用webpack处理stylus文件

1.需要下载一个包 npm i stylus-loader 2.功能介绍 stylus-loader:负责将stylus文件编译成css文件 3.配置&#xff1a; const path require(path);//nodejs用来处理路径问题的模块module.exports {/**入口 */entry: ./src/main.js,/**输出 相对路径*/output: {/**文件输…

【HomeAssistant新版文件管理器】

【HomeAssistant新版文件管理器】 1. 前言2. 地址3. 安装4. 使用方法5. 总结欢迎大家阅读2345VOR的博客【Home Assistant 之QQ邮箱推送提醒】🥳🥳🥳2345VOR鹏鹏主页: 已获得CSDN《嵌入式领域优质创作者》称号🎉🎉、阿里云《arduino专家博主》👻👻👻,座右铭:…

云仓酒庄渠道新发布:安徽、广西、广东三地讲师班会后会圆满落幕

2024年云仓酒庄渠道新动态发布&#xff1a;安徽、广西、广东三地讲师班会后会圆满落幕 随着酒类市场的不断发展和竞争的日益激烈&#xff0c;云仓酒庄始终致力于提升内部团队的专业素养和业务能力&#xff0c;以应对市场变化&#xff0c;满足消费者需求。近期&#xff0c;云仓…

ARMv8架构特殊寄存器介绍-1

1&#xff0c;ELR寄存器&#xff08;Exception Link Register &#xff09; The Exception Link Register holds the exception return address。 异常链接寄存器保存异常返回地址。最常用也很重要。 2&#xff0c;SPSR&#xff08;Saved Process Status Register&#xff09;…

软考75-上午题-【面向对象技术3-设计模式】-设计模式的要素

一、题型概括 上午、下午题&#xff08;试题五、试题六&#xff0c;二选一&#xff09; 每一个设计模式都有一个对应的类图。 二、23种设计模式 创建型设计模式&#xff1a;5 结构型设计模式&#xff1a;7 行为设计模式&#xff1a;11 考试考1-2种。 三、设计模式的要素 3…

HTML静态网页成品作业(HTML+CSS)——非遗昆曲介绍设计制作(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…