Linux:线程控制

目录

线程的相关知识

线程创建

pthread_create

线程关于进程内部资源问题

线程等待

pthread_join

线程退出

pthread_cancel

线程id的理解

pthread_self

线程分离

pthread_detach


线程的相关知识

首先线程是在进程内部执行的是OS调度的基本单位

线程的理解:之所以说线程是在进程内部执行的,是OS调度的基本单位,是因为线程是在进程的地址空间内运行,并且CPU其实是不关心执行流是进程还是线程,只关心PCB

而Linux中,一个进程有一个PCB结构体,即task_struct,以这个task_struct为父亲,不重新创建新的地址空间、页表,而是只创建新的task_struct,与进程的task_struct共用地址空间(mm_struct)、页表,并且通过一定的技术手段,将当前进程的资源,以一定的方式划分给不同的task_struct,上述这样的每一个task_struct就称之为线程


在前面我们学习过,用户视角中:进程 = 内核数据结构 + 该进程对应的代码和数据

并且之前学习进程时,进程的内核数据结构只有一个PCB结构体,今天引入了线程,知道了一个进程的内核数据结构可以有多个PCB结构体

只有一个PCB结构体,称之为内部只有一个执行流,而若有多个PCB结构体,说明进程内部具有多个执行流,其中一个task_struct就代表进程内部的一个执行流


今天站在内核视角:进程是承担分配系统资源的基本实体

因为系统分配的资源都是刚开始的进程向系统索要的,而下面的线程都只是用上面进程所申请的资源,所以内核角度说进程是承担分配系统资源的基本实体

CPU并不关心进程与线程的概念,只关注task_struct

在Linux下,PCB <= 其他OS内的PCB的,因为Linux中一个PCB有可能只会分到一部分资源(因为有多线程存在,可能有多个PCB),也有可能只有一个PCB,能够使用全部的资源,所以Linux中的PCB <= 其他OS内的PCB

因此Linux下的进程,统称为轻量级进程

Linux没有真正意义上的线程结构,因为没有为线程专门创建一个数据结构,Linux是用进程PCB模拟的线程的

所以今后在Linux中不严格区分进程和线程,统一叫做轻量级进程,进程内部只有一个执行流的话,就对应到别的OS中的单进程程序;若进程内部包含多个PCB,就对应到别的OS中的多线程

因此Linux不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口

Linux为了照顾用户的使用,因为用户只清楚进程和线程,也并不知道什么轻量级进程,因此在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用,pthread线程库,即原生线程库


线程创建

pthread_create

创建线程的函数是pthread_create,使用man查看:

需要包含头文件pthread.h

函数参数:

第一个参数:线程ID,pthread_t是无符号长整型的

第二个参数:线程属性(默认nullptr即可,不用管)

第三个参数:是一个返回值为void*,参数为void*的函数指针,表示这个线程要执行进程的一部分代码时的入口地址

第四个参数:就是传递给上面的函数指针的参数,若pthread_create函数创建成功,就会把这个参数传递给函数指针,是一个回调的过程

返回值:

成功返回0,失败返回错误码

下面代码进行验证上述结论:

makefile:(Makefile中,线程库编译时必须要加-lpthread,否则会报错)

mythread.cc:

在新线程创建成功后,pthread_create中的第四个参数就会传给threadRun函数的参数args

mythread.cc代码中,main函数中在for循环中,创建了5个新线程,for循环下面是主线程,新线程和主线程分别打印各自的pid,观察pid是否相同,就可以判断出线程是否在进程里

下面首先make编译:

生成了可执行程序mythread,通过ldd mythread查看,发现使用了pthread库:

运行结果如下:

可以看到,主线程和新线程的pid都为26435,所以可以证明线程在进程内部运行

下面执行ps axj | head -1 && ps -axj | grep mythread:

可以观察到只能看到一个进程,pid为26435,但是右边却有6个执行流

下面使用ps -aL可以查看轻量级进程:

可以观察到确实有6个执行流,且pid都为26435 ,说明这六个执行流属于同一个进程

而上图中PID右边那一列LWP,是属于轻量级进程对应的pid,而第一行的PID与LWP相等,所以第一行的执行流叫做主线程,而下面的五个执行流叫做新线程

所以CPU调度线程时,看的其实是LWP,因为PID是1对n的关系,而LWP是一一对应的

而接下来,如果我们kill -9 26435,观察结果:

通过观察结果,杀死PID为26435的进程,再观察轻量级进程时,发现都被终止了,原因是:当前所有线程都是在进程内部创建的,而kill -9把进程终止了,就相当于该进程的资源要被回收,所以这些线程所需要的代码和数据也就会被回收,因此都被终止了


线程关于进程内部资源问题

线程大部分资源都是共享的,但是也有独自占用的资源

线程独自占用的资源:寄存器(即线程的上下文)、栈、错误码、信号屏蔽字、调度优先级

前两种最重要


线程切换的成本低,理由如下:

第一:线程切换时所用的地址空间、页表等不需要切换,而如果CPU调度时调度的PCB是另一个进程的PCB,那么在调度时就需要整体把CPU内部相关的上下文、临时数据、页表、地址空间等都需要切换

第二:CPU内部是有L1~L3cache(缓存),对内存的数据和代码,根据局部性原理(一条指令附近的代码,有较大的概率被使用),预读到CPU内部
           如果进程切换,原进程的cache就会立即失效,新进程只能重新缓存,所以进程切换的成本更高


线程等待

在上面我们kill -9 [进程pid]后,进程及相关的线程都被终止了,那么如果新线程出现异常呢,下面进行相关验证:

运行结果为:

发现发生了8号错误,并且进程也退出了

所以得到下面结论:

①线程谁先运行与调度器相关

②线程一旦异常,都可能导致进程整体退出

线程在创建并执行的时候,也是需要进行等待的,如果主线程不等待,就会引起类似进程的僵尸进程问题,导致内存泄漏问题

所以主线程就需要等待新线程,接口是pthread_join

pthread_join

使用man查看:

需要包含头文件pthread.h

函数参数:

第一个参数:线程id

第二个参数:开始先设为nullptr

函数返回值:

成功返回0,失败返回错误码

下面使用pthread_join函数进行线程等待:

运行结果为:

可以看到,新线程先退出,主线程等待成功后,资源回收后也退出

通过执行ps -aL | head -1 && ps -aL | grep mythread,也可以观察到线程的情况:

在主线程等待新线程时,可以看到两个线程都在运行,而新线程退出后,主线程等待成功也随之退出,此时再观察左边窗口,就没有正在运行的线程了

这时候还有个问题,我们注意到threadRoutine函数是有返回值的,那这个返回值是返回给谁呢?

当然是谁等待它,就返回给谁,一般是返回给主线程,那么主线程如何获取到呢?

自然就是通过pthread_join的第二个参数,retval

retval的类型是void**,而类型之所以是void**,是因为函数threadRoutine的返回值是void*,而void*的地址类型就是void**了

下面改变代码,获取新线程的退出结果:

由于ret中存的就是threadRoutine函数的返回值,我们只需要将ret强转为long long即可得到结果

运行结果为:

可以看到,主线程成功拿到了返回值5

而上面只是最基础的用法,主线程也可以拿到新线程在堆空间开辟的数据,如下:

在threadRoutine函数中,新线程在堆上开辟了一个data数组,主线程等待结束后获取新线程开辟数组的内容

运行结果为:

新线程如果出现异常,主线程也不需要关注,因为如果新线程出现异常退出了,那么主线程同样会退出,所以并不关心这种情况


线程退出

关于线程退出,第一种是直接使用exit

这种方式不推荐使用,因为在线程中使用exit,是直接将整个进程退出了,没有意义

第二种是使用pthread_exit

pthread_exit就只会退出新线程,进程并不会退出,下面演示用法:

我们在threadRoutine函数中,在cout的上面执行pthread_exit,所以就不会执行下面的cout语句

运行结果为:

成功退出新线程,并在主线程中拿到结果

第三种方式是pthread_cancel

pthread_cancel是取消线程

pthread_cancel

同样需要包含头文件pthread.h

函数参数是线程id

下面演示用法:

threadRoutine函数中,新线程死循环

而主线程在main函数中等待4秒,4秒后执行pthread_cancel函数,退出新线程,然后打印线程id,再打印退出结果,最后sleep3秒后主线程也退出

运行结果为:

可以看到打印出来的线程id为上面非常长的一串数字,打印出来的新线程的退出码为-1

通过两个窗口能够更清晰的看出过程,刚开始两个线程都在运行,主线程4秒后执行pthread_cancel取消新线程,这时主线程正在sleep3秒,所以此时只有一个主线程在运行,3秒后主线程也退出,就没有线程正在运行了

我们可以发现,线程被取消,join的时候退出码是-1,也就是宏定义的PTHREAD_CANCELED,即-1


线程id的理解

在上面打印时,发现线程id是非常长的一串数字,其实线程id本质上就是地址,是在共享区的地址,是为了满足线程的私有栈结构的,主线程使用地址空间原始划分的栈结构,创建出来的新线程则在共享区中开辟一段空间,作为线程的私有栈结构,而这段空间的起始地址就是线程id

pthread_self

线程库中还有一个接口,即pthread_self函数,用于每一个线程获得自己的线程id

用法很简单,如下所示:

有了pthread_self函数,就可以和pthread_cancel结合使用,如:pthread_cancel(pthread_self()),可以线程自己取消自己,当然这种用法并不推荐,只是提及一下,还是建议正常使用,即主线程取消新线程,不要自己取消自己

线程之间是共享全局变量的,而如果在全局变量前加__thread(最前面是两个_),就是让每一个线程各自拥有一个全局的变量,这叫做线程的局部存储


线程分离

上面说到主线程可以pthread_join等待新线程结束,但是如果我们并不关心线程的返回值,这时的join是一种负担,这时我们可以告诉系统,当线程退出时,就自动释放线程资源

我们可以给线程设置分离状态,设置完毕后,主线程就不需要再join新线程了,并且新线程退出后会有库自动回收新线程申请的资源,主线程不用关心

pthread_detach

一般都是由新线程自己分离自己,与pthread_self结合使用

需要注意的是,即使线程分离了,分离以后如果线程中出现异常,依旧会影响进程,所有线程都会退出


  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值