目录
线程的相关知识
线程创建
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结合使用:
需要注意的是,即使线程分离了,分离以后如果线程中出现异常,依旧会影响进程,所有线程都会退出