Linux——线程(2)

在上一篇博客中我介绍了Linux中的线程是什么样的,就如同进程可以通过
fork创建,可以被终止,可以退出一样,线程也可以被我们用户控制,这
篇博客我会介绍线程的控制,并且基于线程的控制所产生的一些问题进行
解决这些问题

线程的控制

1. 线程的创建

我们要学会对线程的控制,首先得创建出来一个线程,所以我们会认识一个接口:

这就是我们创建线程的一个接口,可以看出它是三号手册中的,并不是系统调用,这一点之后会从多方面来解释它。

它的第一个参数就是线程的id,类型是ptread_t,这是一个输出型参数需要用户自己传入,然后它给你带出线程的id。

第二个参数是有关于线程的属性的参数,我们现在不谈论,传参时传空指针就可以。

第三个参数是一个函数指针,这个函数的参数是void*,返回值也是void*,这个函数也就是线程要执行的函数。

这第四个参数就是第三个参数中函数的参数,它是以这种方式来传的。

下面我们就可以来编写一简单的代码了(其实也是我们上一章博客中所写的):

当我们写好代码之后直接使用g++编译的时候会出现下面的情况:

这其实是因为pthead_create函数,它不是系统调用,它是存在于库中的,但是它也不是C/C++标准库,所以我们在编译的时候要加上这么一个选项:

而pthread库它有一种叫法,叫做系统原生库 ,顾名思义就是只要有操作系统,那么就一定会携带这个库。

那么这个库为什么会形成单独的库,而不是和系统代码在一起呢?我们上一章说过Linux中不存在真正意义上的线程,只存在轻量级进程,而对于用户来说,我们刚接触Linux的话哪能知道什么是轻量级进程,我们只知道线程,所以操作系统就必须实现出关于线程的一套接口便于用户使用,这也就会形成了一个pthread库了,不和系统代码在一起是因为Linux中真正意义上根本没有线程这个说法。

a. 多线程的创建

既然能创建一个线程,那我们就能够创建多个线程:

在这段代码所演示的结果中我们可以看到几个现象:

task_thread函数被多个执行流同时执行,那就说明这个函数被重入 了。

CPU对于线程的调度也是随机的,这一点是理所应当的,因为我们刚开始解除CPU的调度的时候,它的调度就不是完全按顺序来的。

假如现在我们对四号线程进行除0的操作:

可以看到一个线程出现异常之后,所有的线程都会被终止,这也印证了我们上一篇博客中所说的。

但是我们发现我们上面的代码有些挫,并且我们要知道线程创建的接口中的那个函数的参数是void*的,这就意味着我们可以传任意类型的参数。那么我们的代码可以是这样的:

这里打印出现不规整的现象也是因为CPU调度的时候时间片到了之后切换线程,然后导致输出时,语言缓冲区中的数据出现了错乱。这也说明该函数中含有输出打印函数,这个输出函数也是不可重入的。

我们此时再让这个主线程打印一下线程的id,同时我们再查看一下线程的lwp:

我们发现线程的id和lwp竟然不是同一个值,而这其中的原委我会在之后讲述,其实线程id它就是一个地址而已。

在这里我们介绍一个接口,它可以获取调用线程的线程id:

我们可以看到主进程中打印的tid和线程函数中所打印的线程id是相等的,并且我们也可以打印主线程的id。

2. 线程的控制

接下来我们要来认识一些关于线程控制的接口。

a. 线程的终止

我们首先要学会线程的终止

凭借return可以直接终止进程:

但是要注意exit函数是用来终止一个进程的,如果把它作用在线程中,它会终止整个线程:

除了利用return之外,我们还可以使用pthread库中的函数pthread_exit:

它的参数跟return 一样能返回函数的返回值:

b. 进程的等待

在对进程的认识中进程退出后如果不回收的话,就会进入僵尸状态,而线程也是如此,但是它不是跟进程一样的僵尸状态,而是是类似于僵尸问题。所以就有了线程的回收函数:

这个函数就能够回收指定线程id的线程,回收成功返回0,回收失败返回一个错误数字:

我们在前面只是提及了一下线程执行函数的返回值,但是并没有说怎么获取,我们可以看到,我们是没有办法在主线程中直接得到这个线程函数的返回值的,所以就有了pthread_join的第二个参数。

线程函数的返回值

我们看到线程函数的返回值类型是void* 类型的,而pthread_join的第二个参数是void**的,这其实就是输出型参数,可以回想一下,当我们函数中有输出型参数时,传的其实是这个参数的指针,所以这里才是void*。当使用pthread_join成功回收线程之后,它的第二个参数就会指向线程函数的返回值:

就如同我们线程函数可以传任意类型一样,意味着我们的返回值也可以返回任意类型:

我们看到确实能够返回任意类型,但是这里的线程id的打印好像有问题,我上面说线程id就是一个地址,这个机器是64位的,所以使用int来存储地址会溢出所以这里我们更改一下:

这里可能会有人有问题,假如线程出了异常怎么办?这个等待函数中好像没有waitpid中的status啊,其实无需担心因为线程异常,整个进程都会退出,这就跟这个线程没有关系了,而是进程的问题了。所以编写线程代码时尽量不要让它出现异常。

c. 线程的分离

我们在使用线程的时候有时候并不需要它返回信息,只需要帮我们完成任务之后退出就可以了,所以为了取消pthread_join的消耗,我们可以将这个线程分离:

这个函数会将指定线程id的线程与主线程分离,然后就不需要关心它的返回和回收它了,这个线程会自动被操作系统回收,且如果主进程是不可以join该线程的。说是分离但其实这些线程还是共享资源,出了异常还是会终止所有进程:

当然这个线程分离函数可以让其它线程分离目标线程,也可以自己分离自己。

d. 线程的取消

这是线程终止的第三种方式:

我们可以来看看一个线程被取消之后,join它会发生什么:

我们看到线程函数种返回的是10,但是结果是-1。

这是因为当线程被取消之后,操作系统会将线程函数的返回值设为-1,而这个-1其实是一个宏:

我们再看线程分离之后能否被取消,以及取消之后能否被join呢?

我们能看到线程分离之后仍就能被取消,但是仍然无法join。

较深层次了解pthread库

我们在了解pthread库中的各种接口,但是这些接口都是三号手册中的接口,而它也不存在于C/C++标准库中,我说它其实是原生系统库。

我们这次从用户的视角跳的更高一些,以俯视的视角来认识一下Linux中的线程。我们知道Linux种并没有真正意义上的线程,它是使用进程模拟实现的线程,从而有了轻量级进程,但是对于用户而言,用户在刚使用Linux的时候可能并不认识什么是轻量级进程,他们只认识线程。所以Linux就必须提供关于能够呈现出线程行为的一套体系。但是因为Linux中没有线程,这个就需要在用户层和系统层之间添加一层,使用这一层来让轻量级进程呈现出线程的行为:

所以,一方面pthread需要向下封装Linux中的轻量级进程,一方面要对用户提供各种线程的接口。

而我们也知道,系统中会存在许多的线程,要完成以上两点,pthread库也必须能够管理这批被模拟成线程的进程,先组织再描述,pthread库中一定会有像struct tcb这样的结构体,这个结构体肯定封装着一个lwp,因为轻量级进程与被模拟出来的线程是一对一的。

而且线程拥有着自己的属性,比如上下文,比如栈空间,这些一定是要被以上的结构体纳入其中的。

在Linux中有这样一个系统调用:

这个是用来创建轻量级进程的系统调用,它也是创建进程整体的系统调用(fork的底层就是它),具体要创建哪一个这一点可以通过它的第三个参数flag实现。

它的第一个参数就是我们线程控制中创建线程所要执行的函数。第二个函数就是线程所使用的栈空间,可以看到它是一个指针。

那么pthread库又是怎么管理线程的呢?

当我们的程序中使用了pthread库之后,我们的程序在执行之后,pthread库也会被加载到内存中,同时与进程的共享区通过页表建立映射,但是我们说了,系统中会有很多的线程,也会有很多的进程,所以一定会出现多个进程地址空间指向跟pthread库建立映射的,这样pthread库就需要管理系统中的所有的线程:

而在实际场景中,pthread库是这样组织线程属性的:

在当线程库与地址空间建立映射之后,线程库中就会创建并初始化出关于线程的各种属性集,然后当再次创建新线程的时候,就会在地址上紧挨着上一个新线程属性集在创建初始化,这样看起来对线程的管理就像对一个数组增删查改一样。

在struct pthread结构体中一定存在这样的字段void* retval来存储线程函数的返回值,从而让pthread_join能够获取返回值。

地址空间中栈只有一个,那么线程的栈空间又是如何分配呢?要知道前面介绍的clone接口中关于栈空间的参数是一个指针,所以线程栈空间可以是在堆上创建,第一个线程使用的就是地址空间的栈区,而新线程的栈空间一般是建立在堆上。

我们看到无论是pthead库的函数接口还是pthread对于线程的组织以及管理的数据,它都是需要在进程的地址空间中的用户区建立的的,所以Linux中的线程也叫用户级线程

理解高级语言中的线程库

我们都知道现在的大部分的编程语言,诸如C++、java、python都支持了对应的线程库,我们现在就来简单的使用一下C++的线程库:

当我们编译好它之后再运行它,发现它运行不了,而当我们的编译选项中加上-pthread后,程序能够正常运行:

由此得知,它也只不过是封装了pthread库从而实现了自己的线程方法而已。

线程局部存储

我们使用一段代码来解释它是什么:

这段代码展示了,线程之间是可以共享全局变量的,但是我们在g_flag前加一个这个选项,:

这个全局变量好像变成每个线程私有的了,__thread是一个编译选项,它会将修饰的全局变量从所有线程共享的状态,变成每个线程独有的一个变量,而这个变量的存储也从全局数据区到了线程局部存储。

最后我在提供两个知识点,可以自行下去验证,在多线程中,线程是可以创建子进程的,这个子进程仍是该线程所属线程的子进程,线程也是可以使用exec*类的接口进行程序替换的,但是一般不建议,因为一个线程切换了代码块之后,可能会影响其他线程的代码执行。

相关推荐
CodeClimb2 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
o(╥﹏╥)14 分钟前
在 Ubuntu 上安装 VS Code
linux·运维·vscode·ubuntu·vs
奶香臭豆腐34 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
不想当程序猿_40 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
不爱学英文的码字机器41 分钟前
[Linux] Shell 命令及运行原理
linux·运维·服务器
cdut_suye1 小时前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
qq_433618441 小时前
shell 编程(三)
linux·运维·服务器
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
捕鲸叉1 小时前
C++软件设计模式之外观(Facade)模式
c++·设计模式·外观模式
Tlzns1 小时前
Linux网络——UDP的运用
linux·网络·udp