您现在的位置是:首页 >学无止境 >Learning C++ No.31 【线程库实战】网站首页学无止境

Learning C++ No.31 【线程库实战】

今天还要努力 2024-10-18 00:01:02
简介Learning C++ No.31 【线程库实战】

引言:

北京时间:2023/6/11/14:40,实训课中,实训场地有空调,除了凳子坐着不舒服之外,其它条件都挺好,主要是我带上了我自己的小键盘,并且教室可以充电,哈哈哈,巴士!老师还是非常善解人意滴,并没有强迫我们听她讲C语言二级相关知识,虽然这种实训本质就是在刷题式教学,通过题目去分析知识点,这种方法为了通过考试肯定是有一定效果的,但也只是通过考试而已,在我看来C语言二级过不过并无太大所谓,好了,这方面不好评价,如果不是自己有报班,也许我可能觉得这样是非常合理的吧!这是该星期第5篇博客,距离6篇差距还是有的 ,并且这篇今天都不知道能不能写完,所以从这个星期看来,实质上距离预期还是很遥远滴,哎!但是一切都还在合理范围内运行,并且自己确实是已经花费了大量的时间,刚看了一下搜狗输入法记录的字数,从星期一到现在,零零总总已经打字3万5,平均每天5000,具体以前巅峰时刻能打多少不清楚了,但,这个星期确实从打字上看,算挺多的了,这里也不过多赘述,正式进入今天的正题,有关C++11中线程库和包装器相关知识,当然还有就是上篇博客剩下的有关lambda表达式相关的知识

在这里插入图片描述

深入lambda表达式

在该篇博客中,重点是学习线程相关知识,也就是C++11中有关线程库使用等知识,但是,在学习线程库相关知识之前,此时我们需要将上篇博客的重点,有关lambda表达式相关的知识在复习巩固的基础上,深入理解。重点是因为线程库相关知识需要和lambda表达式结合使用,并且两者结合使用的效果非常好,所以学习lambda表达式的本质其实是在为线程库的学习打基础,接下来就让我们深入了解一下lambda表达式吧!如下:

如何更好的理解lambda表达式

我们可以从两个方面来深入理解lambda表达式,一个是使用类比的形式,将有关lambda表达式的使用类比成一个或者一类以前学习过的知识的使用,从而更加熟练的掌握lambda表达式的使用技巧,而另一个则是通过搞懂其对应的底层知识,从而实现深入了解该知识,所以此时我们就从这两个方面来进行讲解,深入理解有关lambda表达式的知识。

1.使用类比
回顾lambda表达式使用方法的同时,将其与函数使用方法进行比较,此时我们发现,定义一个lambda表达式和定义一个函数在许多方面极为相似,如参数列表()和函数体{},所以我们就可以很好的将一个lambda表达式理解成是一个lambda函数或者是一个lambda匿名函数对象,具体如下图所示:
在这里插入图片描述

从上图中,此时我们就可以看出,当我们使用 lambda语法[]()->{} 定义了一个简单的lambda表达式时,此时这个表达式不仅可以理解成是一个lambda函数,也可以理解成是一个lambda匿名函数对象 。具体该表达式代表的什么意思,就需要通过不同的场景来判断,同理,如上图所示,如果当其是作为一个赋值对象的话,那么此时它表示的意思就是lambda函数,并且具体意思就是在使用该lambda函数定义一个函数对象,如果其有进行具体的参数传递,那么此时表示的意思就是匿名函数对象,并且具体意思就是使用该匿名函数对象去调用对应的lambda函数,所以总而言之:一个lambda表达式具体代表什么意思,需要通过不同的场景来分析。

2.深入底层
明白了上述知识,此时我们就知道,一个lambda表达式不仅可以是一个lambda函数,也可以是一个lambda匿名函数对象,那这是为什么呢?想要明白这个问题,那么就需要通过了解编译器底层对lambda的封装来看,如下图所示:

仿函数汇编代码
在这里插入图片描述
lambda表达式汇编代码
在这里插入图片描述
此时从上述两幅图中,我们就可以看出,无论是使用仿函数,还是使用lambda表达式,两者的汇编代码在本质上来看都是相同的,都是去调用对应类中的构造函数和仿函数("()"运算符重载)。只不过使用仿函数的方法,对应的类,是我们自己编写的,而使用lambda方法,对应的类(lambda_uuid)却是编译器根据对应lambda表达式中的内容生成的,所以可以充分表明,lambda表达式在底层就是一个仿函数,由于lambda表达式本质是一个仿函数,所以此时它就不仅可以被理解成是一个函数,也可以被理解成一个匿名函数对象,并且此时我们从另一个方面也可以很好的证明lambda表达式是一个仿函数,如下:

首先我们都知道,想要实现一个仿函数,就需要重载一个()运算符,想要重载一个()运算符,就需要封装一个类,并且是一个没有任何成员变量的类,所以得出一个结论,实现仿函数的类,它的大小一定是1个字节,所以此时根据这个特征,我们就可以去计算一个lambda表达式的大小,如果该lambda表达式的大小为1,那么就可以很好的说明,lambda表达式的底层就是一个仿函数,如下图所示:
在这里插入图片描述
发现,一切都在我们的结论之中,所以再一次证明,一个lambda表达式的底层就是一个仿函数而已(类似迭代器和范围for),通过上述知识对lambda表达式的分析,无论是从底层还是表面,我们都进行了较为深刻的分析,那么lambda表达式相关的知识,我们就讲解到这吧!接下来让我们正式进入该篇博客的重点:有关线程库相关的知识

注意: 上述所说的uuid本质就是一个系统生成的唯一识别码,感兴趣的同学,可以操作该链接:什么是uuid,当然使用uuid的目的也就是让编译器生成一个具有唯一标识符的lambda_uuid类,便于编译器生成和管理(类似时间戳)

线程库相关知识

在没有具体学习过操作系统底层有关线程的知识时,线程相关内容对于我们来说有较大的学习成本,所以有关线程相关的知识,在该篇博客中,我们只能意会不可详解,具体需要本博主神功大成之后,才能搞定线程相关知识在C++11编码中具体的情况,否则对于C++11编码中有关线程加锁、解锁、原子性问题和具体运行原理,我们都需要花费大量的精力,并且可能讲解的还不是很正确和详细,所以这部分知识必须等我学习完系统编程中有关线程的知识之后,再学习。现在我们就简单的先了解一下有关线程库和线程库如何使用等相关知识,如下:

为什么C++11要新增一个线程库

首先明白,在C++11之前,C++语法中并没有属于自己的线程库,而是需要去调用系统级别的多线程库或者API,但,此时由于操作系统不同的问题,就会导致如果两个操作系统使用的POSIX(可移植操作系统接口)不同,就会导致同一份与线程相关的代码,在不同的操作系统上运行不了,所以为了解决这个问题,在C++11中就推出了属于自己的线程库(thread),这样就可以不需要再使用条件编译指令来兼容两个不同的操作系统,如下图所示:
在这里插入图片描述
而是可以直接使用C++11推出的thread库,但明白,thread库的本质依然还是需要使用条件编译指令去封装,总而言之,在C++11中有了对应的线程库之后,我们在编码时,就不需要再自己去实现条件编译指令,让同一份代码可以在不同的操作系统下运行啦!欧耶欧耶欧耶耶耶!

C++11中的线程库

学习C++11中的线程库,最重要的自然就是去C++文档中研究一下该线程库中具体的接口如何进行传参,这样我们就可以在最短的时间内掌握对应接口的使用方式,如下图所示:
在这里插入图片描述

当然由于C++是一个面向对象的语言,所以线程库在C++11看来,本质就是对其进行封装,实现成类的形式,然后通过对象的方式进行调用,所以上述就是thread类中的构造函数,当然该构造函数的作用就是构造出一个对应的线程,具体使用如下图所示:
在这里插入图片描述
可以看出,用法非常的简单,上述代码表示的意思就是生成两个线程,让这两个线程去调用test_thread函数,然后传对应的参数,从表面上看平平无奇,那是因为我们没有对应着该线程接口的构造函数来看,如果详细的去分析该构造函数,那么此时你会发现,这个构造函数非常的高级,如下图所示:
在这里插入图片描述
首先明白,该线程构造函数是一个explicit类型的构造函数,那就表明,此时该构造函数不允许隐式调用,而只支持显示调用。其次可以看到,该构造函数的参数有两个:Fn&& fnArgs&&... args 一眼看过去,就能看明白,这些知识都是我们之前在C++11中学习过的,其中第一个参数要明白,它是一个模板可变参数,并且是一个完美引用,结合上述的基础使用,此时就可以发现,该参数接收的就是test_thread这个函数,而第二个参数,仅仅就是一个可变模参数模板而已,和我们之前学习的一样,使用了可变参数模板,那么就可以进行任意类型,任何数量传参,所以这也是为什么上述线程基础使用时,可以进行随机参数传递的原因(对应函数要几个,我就可以传几个的意思)。通过分析,此时可以看出该线程构造函数的重点在第一个参数上,第二个参数只是用于配合第一个参数使用而已,那么第一个参数具体表示什么意思,具有什么作用呢?如下:

详解Fn&& fn
通过上述简易分析,此时知道,该参数其中的一个作用可以是接收函数指针,在这里讲到函数指针,那么我们就必须扩展详解有关函数指针的问题,明白,上述test_thread是一个函数名,函数名是一个函数地址,而地址等价于指针,所以函数地址等价于函数指针,函数名等价于函数指针,所以虽然上述使用thread进行传参时,使用的是函数名,但是本质在编译器看来,它是一个函数指针类型,明白了这点之后,此时很多同学对于上述代换可能存在疑问,那么身为最具责任心的博主之一,我责无旁贷的将有关知识进行详解,如下:

函数、地址和指针关系详解
1.函数名是函数地址理解:因为在编译器看来,如果要将对应的程序加载到内存中,那么该程序中的所有函数就会被转化成一串二进制序列,所以此时就会导致函数名被转化成该二进制序列的起始地址,所以我们可以将函数名作为函数地址来使用

2.函数地址是函数指针理解:这个问题可以替换成指针是地址的理解,首先指针变量本身就是内存中的一个地址,并且它可以指向另一个地址,存储另一个地址,从而实现访问该指针就是在访问该指针指向的地址,别的先不多说,从我学习这么久以来,这种语句(xxx就是xxx)。我一共接触过三种,其中一种就是上述所说的指针,而另外两种分别是索引和映射,而指针只是一种抽象概念,实际上并不存在,而索引和映射虽然也是概念,但是它不是抽象概念,而是逻辑概念,所以从客观上分析,指针的本质就是一种映射,指针指向某个地址,存储某个地址,本质都是通过映射来完成,所以指针变量和其它变量在内存存储上是相同的,占据着4/8个字节,在使用上不同,指针变量拥有映射内存地址的功能,因而从本质上来看,指针就是地址。

搞定了上述有关函数指针、函数名、函数地址相关知识的理解,我们回到正题,继续分析Fn&& fn相关知识,由于Fn此时是一个模板参数,并且C++支持统一的函数调用语法,所以明白,此时该参数不仅可以支持接收函数指针类型,它还支持接收函数对象或者是lambda表达式,如下图所示:

使用lambda表达式初始化线程
在这里插入图片描述
并且此时上图代码表示的意思就是:让线程1和线程2分别从0打印到n1次和n2次,所以此时我们明白,thread支持使用函数指针、函数对象和lambda表达式进行初始化,并且其中thread构造函数的第一个参数表示的就是对应的可调用对象,并且明白,由于主线程(main)有return与其对应,为了防止主线程结束子线程还没有结束,此时就一定需要在主线程结束之前让子线程结束,也就是上述线程库中的join()接口,所以上述有关thread构造函数我们就讲解完了,接下来让我们一起看看thread中的其它类似于join()的接口,如下图所示:
在这里插入图片描述
以上就是线程库中的所有接口,具体分别是什么作用,这里不详细介绍,同理上述开头所说。

线程库具体使用方法

行文来到这里,我们大致明白了线程库的使用,但都是最为基础的演示,并非是真正的编码使用,所以接下来,我们就深入看看线程库如何被使用
在这里插入图片描述
所以新知识又增加啦!明白,线程最经典的使用方式,就是将该线程给放到对应的容器中使用,这样就可以实现多线程同时运行,当然这也就是线程最为关键,最重要的一个点,也就是我们学习线程的原因,因为线程可以极大的提高计算机在各个领域的效率,具体需要等我们学习了系统编程中的线程,才可以更好的理解它。

此时明白,上述有关线程的知识,只是C++11中最为简单的用法,具体线程这个东西,还是较为复杂,同理该博客开头所说,这里我们先不做详解,具体要等我们将系统编程中线程的知识学完,到时候神功大成,再来搞定该块知识,具体就是一些像无锁编程、加锁、解锁、CAS、线程安全等知识,接下来就让我们进入另一个重点,有关包装器的知识吧!

什么是包装器

包装器(function) 也叫作适配器,本质是一个类模板,可用于封装某种类型的对象或数据,并提供额外的功能或接口来简化应用程序的使用或增强对象的功能,具体使用如下图所示:
在这里插入图片描述
同理,此时我们就可以使用额外的f1、f2、f3变量来调用对应被包装的函数接口,并且此时如下图所示,可以使用函数指针,仿函数或者lambda去初始化变量,也就是上述所说,使用额外的功能来增强对象功能
在这里插入图片描述
从上图可以看出,包装器可以让我们将函数指针、仿函数和lambda表达式这种本不能用于初始化对象的结构,可以用于初始化对象,如上述map<string, function<int(int, int)>> m;所示,此时我们就可以直接使用包装器,让函数指针、仿函数和lambda表达式去初始化map对象,然后再根据map中方括号运算符("[]")的特性,再根据对应的名字去map中查找对应插入的函数类型,最后直接通过匿名对象进行调用,所以包装器此时最大的优点就是可以让我们更好的控制可调用对象的类型,不再只是容器和变量,而是各种函数,总之包装器能解决很多编码上的不足,具体场景,下篇博客见。

总结:该篇博客在于了解线程相关知识,至于最后有关包装器的知识,后序文章还有详细介绍,此处就作为简单了解就行,由于时间问题,该篇博客就这样吧!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。