学习系统编程No.28【多线程概念实战】

news2024/9/29 3:25:43

引言:

北京时间:2023/6/29/15:33,刚刚更新完博客,目前没什么状态,不好趁热打铁,需要去睡一会会,昨天睡的有点迟,然后忘记把7点到8点30之间的4个闹钟关掉了,恶心了我自己一早上,真的是罪过呀!极度没睡好加没睡够,由于上篇博客马上就可以完成,所以中午没有选择睡觉,而是想着更新完再睡,但是现在困意不是很重,所以趁着这个没什么状态期间,将该篇博客的引言写写,然后把git提交一下,并且重点是今天出成绩啦!在持续摆烂中,和预想的一样,考的不怎么样,不过好在都及格了,其中高数应该是老师捞起来的,低分飘过,哈哈哈!摆烂过程只有我自己知道,具体不好形容,所以只要没有挂科我就已经很满足了,不过对于我来说更重要的是,信号有关知识在上篇博客我们就全部学完了,接下来终于可以进军多线程的学习了,这点令我非常的激动,不然博客标题都不知道怎么命名了,哈哈哈!正式进入该篇博客的学习,有关Linux系统下多线程的知识!

在这里插入图片描述

什么是可重入函数

可重入函数是一个新的概念,伴随着可重入函数,那么自然就有不可重入函数,那么具体什么叫可重入函数,什么叫不可重入函数,需要我们进行一定的铺垫,才能搞清楚,首先由于该知识点是在信号相关知识,并且与内核态和用户态之间发生进程调度相关,所以此时我们明白第一点,就是当一个进程,时间片到了,就会发生进程调度,也就是保存当前进程的上下文,替换下一个等待进程,当然进程调度也就是让用户态进程切换为内核态进程,而如果是进行用户态到内核态进程的切换,那么同时就会导致操作系统对该进程进行信号检测,此时就会发生信号递达(默认动作,自定义动作,忽略),按照该场景,此时就会产生一个非常细节的问题,如下图所示:

在这里插入图片描述
同理,如图所示,此时就可以发现,如果在插入接口时,发生了进程调度,从而导致操作系统进行信号检测,执行对应信号的自定义动作,且刚好该动作,也是一个插入动作,那么就会导致上图所示问题,也就是内存泄露问题。显然,该问题的存在是不合理的,并且该不合理现象是由于同一个函数被重复进入导致,所以对于上述这种函数,一但被重入就会导致一系列的问题,那么这种函数就被称为不可重入函数,反之称为重入函数,也就是同一个函数被重入,不会出现问题的函数。同理,明白了这点之后,什么是可重入函数,什么是不可重入函数,我们就搞定啦!并且一般常见的不可重入接口都在各种容器中,如上述的链表,当然还有STL中各种容器的接口,还有malloc等!

理解volatile关键字

首先明白,这个关键字是C语言中的一个关键字,本质就是为了告诉编译器某个变量在程序执行过程中可能会被修改,要进行特殊处理,不能只是盲目的从CPU中的寄存器获取该变量,而是要重新从内存中获取,具体是什么意思呢?想要搞懂这个关键字的概念,需要从下述代码入手,如下:

在这里插入图片描述
从上图中可以看出,使用了gcc编译器中的优化级别对比没有使用gcc编译器的优化级别,两者在代码执行结果方面,有很大的不同,并且从运行结果来看,没有使用gcc优化,那么程序就会按照我们的预期,在收到了2号信号后,发生自定义动作捕捉,然后将quit置1,紧接着返回继续执行代码时,退出死循环,程序正常退出。而如果使用了gcc编译器的优化功能(-O2),此时从运行结果来看,无论是否收到2号信号,那么程序都一直处于死循环,本质也就是当该进程收到2号信号后,执行自定义动作捕捉时,quit全局变量并没有由0置1,导致循环不会退出,那么此时问题就来了,为什么执行了自定义动作捕捉,也就是执行了quit置1的代码,循环不会终止呢?还有就是为什么只有使用了gcc优化功能才会导致这个问题呢?具体如下图所示:

在这里插入图片描述
从上图有关代码在硬件上的执行过程,我们就发现,本质就是因为使用了O2的优化级别,从而导致CPU在进行计算时,不直接从物理内存中获取相关变量的值,而是直接从寄存器中获取先关变量的值,从而就会导致,因为信号执行自定义动作捕捉让quit全局变量由0置1,单单只是让物理内存上的quit变量置1,而没有让CPU寄存器中的quit变量置1,从而导致quit置不变,程序持续死循环,所以上述两个问题,就非常容易回答,本质就是因为使用了gcc的优化功能,会导致CPU在计算时,不直接从物理内存中获取数据,而是直接从寄存器中获取,所以也可以得出结论,gcc的优化功能本质就是在优化物理内存不断加载数据到CPU寄存器中的这个过程,从而导致意料中改变的值,CPU接收不到。当然注意:不是所有的代码都能像上述一样通过gcc的优化功能进行优化,从而导致寄存器中的数据不会被物理内存中的数据影响,只要像上述代码中while(quit != 0);这样频繁执行同一结果的代码,才有资格被优化,也就是减少频繁从物理内存加载数据到CPU寄存器中,从而提高效率。明白了上述知识之后,无论是上述的现象还是问题,我们就都搞定了,当然搞懂了上述问题和现象,volatile就不是什么重点了,同理上述所说,使用volatile就是在告诉编译器,每次进行计算时,都要去物理内存中获取数据而已(保证内存可见性)。所以当我们使用了volatile关键字,上述因为gcc优化功能导致死循环的问题就可以很好的被解决,如下图所示:
在这里插入图片描述

切记: 使用gcc优化的本质,还是在通过代码控制具体的执行方式,并且这个代码是在生成汇编指令的时候添加进去的,也就是我们的代码因为优化变成了一套更复杂和高级的代码,从而导致CPU在执行该代码时,变成优化形式执行(也就是不从物理内存中读取数据,而是从寄存器中读取),所以CPU具体执行代码如下图所示:
在这里插入图片描述
最终明白,在gcc中有许多的优化级别,具体如下图所示,这里不多过讲解:
在这里插入图片描述

SIGCHLD信号

搞定了上述相关知识,此时我们正式进入信号有关知识的最后一个知识点,与子进程退出相关的信号,在之前的学习中,我们学习了进程创建,进程等待等一系列知识,明白子进程需要被回收(父进程等待),不然就会导致僵尸进程等问题,并且父进程在等待子进程时,有两种方式,一种是阻塞式等待,一种是轮询式等待(非阻塞式),从而导致父进程想要成功的回收子进程就一定要牺牲一定的效率,那么如何可以避免这个问题呢? 首先明白父进程需要等待子进程的本质原因在于父进程并不知道子进程在干嘛,进而不知道对应子进程在什么时候会退出导致。明白了这点之后,我们就可以将问题转移为:子进程在退出时,是不是安安静静什么都不干,就默默的退出?答案肯定不是,那么子进程在退出时,会干什么呢?通过这个问题,我们就可以很好的引出该知识点的主角:SIGCHLD信号,明白子进程在退出时,它会发送一个SIGCHLD信号给父进程,但由于父进程对于该信号的默认处理动作是忽略,所以在我们看来,子进程退出时是默默无闻的退出,那么如何证明,子进程确实会发送SIGCHLD信号呢?如下代码所示:
在这里插入图片描述
整体代码非常简单,就是在一个程序中创建一个子进程,然后让该子进程退出的同时,父进程循环运行(防止孤儿进程),然后对SIGCHLD信号进行捕捉,看父进程是否会执行对应的自定义动作,当然,如果执行了,那么就表示父进程确实收到了子进程退出时,发送的SIGCHLD信号,反之没有。总之,目前从上图运行结果可以看出,父进程确实收到了SIGCHLD(17)信号。表明,子进程在退出时,确实不是默默无闻的退出,而是会发送SIGCHLD信号给父进程。搞清楚了这点之后,接下来就是顺水推舟,我们水到渠成的就可以搞定有关SIGCHLD相关的知识啦!还是从子进程退出时会发送一个信号给父进程出发,首先这样就可以解决我们上述父进程需要浪费效率等待子进程的问题,现在因为子进程会发送信号给父进程,那么父进程就不需要再浪费资源去等待子进程,而是等子进程退出,发送SIGCHLD信号给父进程时,父进程再去回收它,具体代码如下所示:
在这里插入图片描述
如上代码所示,我们在等子进程退出,父进程接收到SIGCHLD信号,执行自定义捕捉动作时,在该自定义动作中进行子进程回收,当然也就是使用waitpid接口等待子进程退出,并且明白如果等待成功,那么waitpid接口就会返回对应被等待进程的进程pid,所以使用该方法到底能不能成功等待子进程退出呢?如下图运行结果所示:
在这里插入图片描述
如图可以发现,最终waitpid的返回值和进程的pid值是相同的,并且代码执行了3秒之后,由于父进程没有立即回收,而是等待了一秒才回收,所以子进程在第四秒时,是处于僵尸状态,而在第五秒,父进程开始回收子进程时,子进程才从僵尸状态被父进程回收,父进程继续运行。

总而言之,上述通过子进程退出,发送信号的方式,我们就可以让父进程不需要特意的去关心子进程是否退出,所以当我们的父进程在需要执行很多代码的情况下,此时就可以使用上述方法,通过信号来回收子进程。

但是当我们使用信号的方式来回收子进程,按照上述代码来看就会存在一定的问题,当然这个问题是存在于不同的情况下,也就是当我有多个子进程需要被回收的情况下,凭借以前学过有关信号处理的知识,我们知道一个进程的pending位图只能记录一次信号,当一个信号正在被执行时,该信号就会被添加到信号屏蔽字中(block位图),那么就会导致父进程不能同时处理多个子进程发送过来的SIGCHLD信号,那么此时就会导致某些子进程的SIGCHLD信号被遗漏,从而导致某些子进程不能被回收,最终造成僵尸进程问题。所以为了解决该问题,我们需要将上述代码进行一定的升级处理,如下代码所示:
在这里插入图片描述
注意:上述代码有两个知识点,一是waitpid的第一个参数使用-1就可以在不需要指定进程pid的情况下去等待任意进程,二是在使用waitpid接口时,第三个参数我们最好是使用WNOHANG参数,表示非阻塞等待,也就是只等待退出进程,不会等待未退出进程,这样可以使代码更加安全。

注意: 除了之前学习的回收子进程知识和今天学习的回收子进程知识,在Linux系统内部还存在一种回收子进程的方式,就是让父进程去调用sigaction或者signal接口将SIGCHLD信号的处理动作设置为SIG_IGN(忽略),这样fork出来的子进程在进程终止时,也会自动被操作系统清理,不会产生僵尸进程,也不会通知父进程。

线程基础知识学习

该篇博客来到这里,上述有关可重入函数,volatile和SIGCHLD信号相关知识就搞定啦!接下来正式进入该篇博客的主题,有关线程相关知识的学习,当然由于线程相关知识非常的繁杂,所以一篇博客肯定是搞不定的,并且由于我们是刚开始学习线程相关知识,所以肯定是由浅入深 ,先学习一下线程基础知识,大致了解一下线程的概念及其使用,具体如下所述:

Linux线程概念

什么是线程
首先明白操作系统相关的知识被称为是计算机里的哲学,就是那种读一遍过去,你感觉,嗯,很有道理,但是却不知道是什么意思,不知道怎么用,然后学了等于没学,哈哈哈!下述几个就是经典操作系统书籍中对线程的简单描述:

  • 线程是一个执行分支,执行粒度比进程更细,调度成本更低
  • 线程是进程内部的一个执行流
  • 线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体

明白了上述操作系统中对线程的描述,也就是那几句哲学一样的语句,此时如何理解呢?下面我们就将这几句话通过经典的场景来分析,进而搞懂这句话的深层含义,如下:

1.如何理解线程是一个执行分支,执行粒度比进程更细,调度成本更低
想要理解该知识点,首先需要明白CPU中有两类寄存器,一类是可见的,一类是不可见的,也就是有的寄存器是暴露给我们,允许我们使用的,有的寄存器是由CPU自己做管理,不提供给我们使用的,明白了这点之后,我们就可以来谈谈线程相关的知识了,每个线程都具有独立的执行上下文和栈空间,而在线程在运行时,寄存器就为线程的上下文切换提供存储环境,从而保证线程的运行环境,如下图所示:
在这里插入图片描述
从图中可以看出,寄存器为进程和线程的运行提供了存储环境,以便于CPU执行相应的代码,而线程是根据进程的pcb和操作系统中对应的代码创建出来的,并且对于进程来说,线程的特点就是只有根据pcb创建出来的TCB,没有对应的虚拟地址空间,它们的虚拟地址空间是和进程共用的。从而导致多个线程可以同时共享同一块地址空间上的栈空间和代码段,并且操作系统通过一些列的操作,可以将代码段上的代码分配给每一个线程去执行(复杂),让每一个线程都拥有自己的上下文和栈空间,所以可以将线程看做是进程的一个执行分支。按照上图所示的话,那么该进程此时就拥有了4个执行分支,从而导致代码的执行效率大大提高。当然执行效率的提高虽然和执行分支增多有一定关系,但是使用线程的好处远远不止于此,重点在于线程的执行是并发执行,具体如何并发执行以及并发执行等细节相关知识,需要等我们深入学习之后再来详谈,此时我们只要知道,由于线程是并发执行的,所以导致线程不仅可以共享同一块地址空间,而且也可以共享同一个时间片,此时就会导致线程在调度时,不需要像进程调度时一样,需要切换地址空间,更改映射关系等!而是直接使用同一地址空间,这样就可以让线程调度成本大大降低,当然有关线程调度成本方面的问题,并不止于此,此处还涉及到一个CPU获取数据时的局部性原理,下文慢慢谈到。

2.如何理解线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
明白了上述有关线程的概念,那么进程到底是什么呢?进程也是一个线程吗?这么理解肯定是不对的,因为线程是通过进程创建的,它们之间的关系肯定不是对等的,而应该是上下级。所以当我们谈到一个进程时,那么该进程一定是需要包含对应的执行流(线程)、地址空间,页表、物理内存等,并且对于进程的概念我们就需要进行升级,从之前的单执行流,理解为包含一大推东西的一个实体,所以也就是将进程理解为是承担分配系统资源的基本实体(如上图一般)。并且注意:同理时间片的分配,操作系统在分配系统资源时(内存资源、CPU资源等),是以进程为基本单位进行分配,只有当有了进程之后,相当于就是有了系统资源之后,我们才能根据进程去创建线程,也就是让线程去向进程申请资源,当然可以理解成是分配它的资源。同理如上图所示,在CPU看来,它识别的要么就是一个单独的进程执行流,要么就是该进程中对应的一个线程分支,而单独的一个进程执行流在我们看来和一个线程没有区别,所以对于CPU来说,调度的基本单位就是线程。

如何理解局部性原理
同理,首先明白,在CPU中不仅包括了上述所说的寄存器和之前所说的MMU(内存管理单元),其中还包括了运算器、控制器、高速缓存(cache L1,L2,L3)等!因为操作系统为了提高代码的执行效率,会将某些热点数据先加载到缓存中,也就是当我们在执行某段代码的时候,操作系统会将该代码附近的代码加载到缓存中,这样就可以让CPU上对应PC指针等指向对应执行代码的概率增大,从而提高代码执行效率,这就叫局部性原理。

总而言之,可以将线程理解为在一个进程内部独立执行的子执行单元。一个进程可以包含多个线程,每线程都有自己的执行路径和执行上下文,和对应的进程共享系统资源,并且可以实现并发机制,极大提高代码执行效率和资源管理方式。

总结:由于时间关系,该篇博客就到这啦!线程相关知识我们算是开了个小头,下篇博客更精彩哦!详解页表映射和线程的相关使用等知识!反正都是干货,一起期待吧!

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

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

相关文章

C语言学习(三十)---枚举、位段、联合体

这几天在往实习的地方弄东西,比较累,因此没有更新,在几天前我们学习了内存操作函数,其与之前学习的字符串操作函数相比,适用范围更加广泛,大家要注意掌握学习,今天我们将学习枚举、位段和联合体…

闲置BROOKSTONE Rover间谍车重生记

22年春节在家,哪也去不了,收拾出来一个多年前的玩具,全名叫BROOKSTONE Rover revolution,长这个样子。 尽管是7年前的产品了,科技感依旧挺足 印象中能手机控制,并且能语音对讲。只是网上找到的安卓版应用已…

xenomai内核解析--xenomai实时线程创建流程

版权声明:本文为本文为博主原创文章,未经同意,禁止转载。如有错误,欢迎指正,博客地址:https://blog.csdn.net/qq_22654551?typeblog 文章目录 问题概述1 libCobalt中调用非实时POSIX接口2 阶段1 linux线程…

02_jQuery与Ajax

jquery jquery的作用 他是js的库 处理html,事件,实现动画效果,方便的为网站提供AJAX交互 命名格式 .ji:体积大,用于学习和debug使用 .min.js:压缩的文件,体积小,用于线上环境使用 使用方法 必须先在页面文件中进行引用 $就是jQuery 注意: jQuery是DOM的封装 jQuery和…

Spring Boot 中的服务网关是什么,原理,如何使用

Spring Boot 中的服务网关是什么,原理,如何使用 在微服务架构中,服务网关是一个非常重要的组件。它可以作为所有微服务的入口,负责路由、负载均衡、安全性和监控等方面的功能。Spring Boot 提供了一系列的服务网关工具&#xff0…

redis-哨兵安装

解决问题 自动故障修复 1.在主从模式的基础上,在主节点添加自己的认证密码即可 2.将代码客户端地址改为哨兵地址 ------------- 主节点配置 daemonize yes port 6379 bind 0.0.0.0 requirepass 123456 save 3600 1 300 100 60 10000dir /usr/local/redis dbfilename dump.r…

Java POI (4)—— Linux环境下文件解析过程出现OOM的问题

Excel文件在进行解析的时候,在Windows环境中,没用报错,但是在Linux环境中,出现了如下的报错: nested exception is javalang.OutofMemoryError: Java heap space (OOM) 一、内存溢出和栈溢出有什…

主流特征工程平台(一)

一. 目标 对于Feature Store的能力与边界,每家的定义略微不同,《Feature Stores - A Hierarchy of Needs》)这篇文章做了很好的总结,大体分为如下几个层次: 特征管理:特征抽取、处理、存储、元数据管理&am…

群晖NAS 安装 MySQL 远程访问连接

目录 1. 安装Mysql 2. 安装phpMyAdmin 3. 修改User 表 4. 本地测试连接 5. 安装cpolar 6. 配置公网访问地址 7. 固定连接公网地址 [TOC] > **转载自cpolar极点云文章:[群晖NAS 安装 MySQL远程访问连接](https://www.cpolar.com/blog/install-mysql-remote-…

Spring 事务使用详解

前言 什么是事务?根据 维基百科事务 介绍,数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。简单来说,事务就是将一系列操作当成一个不可拆分…

报喜鸟如何触发数字化转型及品牌扩张战略分析

传统服装企业往往面临缺乏创新、盲目扩张、追求低成本、库存和行业周期性等问题。报喜鸟通过深入分析市场需求,明确聚焦主业,提出加强品牌核心竞争力的价值主张。通过实施DTC转型,发力电商平台和线下门店等举措,报喜鸟成功提高品牌…

VSC++=》 指针实数排序

缘由https://bbs.csdn.net/topics/396523482 void 指针实数排序(double* aa, int d) {//缘由https://bbs.csdn.net/topics/396523482double lin 0; int j d, jj 0;while (jj < d) if (--j > jj) if (aa[j] > aa[j - 1])lin aa[j], aa[j] aa[j - 1], aa[j - 1] …

Flask request和requests(客户端服务器)

Flask request和requests 1、Flask request属性2、requests属性3、实现代码 1、Flask request属性 这么多属性什么时候有值什么时候没值&#xff0c;其实完全取决于我们请求头content-type是什么&#xff0c;如果是以表单形式multipart/form-data、application/x-www-form-url…

辅助驾驶功能开发-功能规范篇(21)-3-XP行泊一体方案功能规范

XPilot Parking 自动泊车系统 七、全自动泊车(AutoParking) • 自动泊车辅助(AutoParking Assist)、斜列式车位泊车辅助(Diagonal AutoParking Assist) - 产品定义 基于超声波传感器和环视摄像头对空间和车位的识别,通过自动泊车系统实现全自动泊车入库。 - 功能说…

使用HHDESK图形化功能管理服务器

服务器的管理通常繁琐而枯燥&#xff0c;需要大量的命令行来执行。 所以图形化功能应运而生。 本篇以传输文件为例&#xff0c;简单介绍一下HHDESK的图形化管理功能。 首先需要配置好服务器。 点击连接管理&#xff0c;在连接类型中选择SSH&#xff0c;按照刚才在服务器中配…

方波信号轨迹跟踪(过冲与圆角)

在控制系统中&#xff0c;方波信号轨迹跟踪可能会面临过冲和圆角的问题。过冲是指跟踪信号超过期望值的现象&#xff0c;而圆角是指在方波信号变化时产生平滑的过渡。这些问题主要是因为传统的控制方法无法完美跟踪非线性的方波信号导致的。 过冲通常也称为超调。在方波信号的…

C++中的vector使用详解及重要部分底层实现

本篇文章会对vector的语法使用进行详解。同时&#xff0c;还会对重要难点部分的底层实现进行讲解。其中有vector的迭代器失效和深拷贝问题。希望本篇文章的内容会对你有所帮助。 目录 一、vector 简单概述 1、1 C语言中数组的不便 1、2 C中的动态数组容器vector 二、vector的常…

vue中实现div可编辑,并插入指定元素,样式

前言&#xff1a; vue中实现一个既可以编辑内容&#xff0c;有可以动态编辑内容插入一个带有样式的内容&#xff0c;改变默认内容后&#xff0c;这个样式消失的效果&#xff0c;这里来整理下调研与解决实现问题之路。 实现最终效果&#xff1a;图2为默认内容 1、可以光标点击任…

自定义MVC框架优化

目录 一、前言 二、优化问题 1.子控制器的初始化配置问题 2.页面跳转优化代码冗余问题 3.优化参数封装问题 三、进行优化 1.解决子控制器初始化配置 2.解决页面跳转的代码冗余问题 3.解决优化参数封装问题 4.中央控制器 一、前言 在自定义MVC框架原理中讲述了什么是…

Redis - Redis GEO实现经纬度测算距离,附近搜索范围

Redis GEO 主要用于存储地理位置信息&#xff0c;并对存储的信息进行操作&#xff0c;该功能在 Redis 3.2 版本新增 一、Redis GEO 操作方法 geoadd&#xff1a;添加地理位置的坐标 geopos&#xff1a;获取地理位置的坐标 geodist&#xff1a;计算两个位置之间的距离 geor…