线程已经成为调度的基本单位了,每一个线程都属于同一个地址空间中,所有的线程都属于同一个进程
换句话任何一个线程尝试调用geipid它应该是同一个pid
可是OS选择线程时,他怎么知道哪个线程是主线程?哪个是新线程?线程也有主次之分
每个线程都是一个调度的基本单元,所以每个线程都要有自己调度的id值啊?反正记着每一个线程也要有一个id
正是因为OS用进程内核数据结构模拟的线程,所以内核中有没有很明确的线程的概念呢?
没有的。它只有一个轻量级进程的概念
既然他只有轻量级进程的概念的话,注定了linux os不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用!
可是我们用户需要线程的接口!
linux程序员提供了pthread线程库 – 应用层 – 轻量级进程接口进行封装。 为用户提供直接线程的接口
这是一个第三方库,几乎所有的Linux平台,都是默认自带这个库的!
Linux中编写多线程代码需要使用第三方pthread库!!
那到底怎么使用这个库呢?
快速使用一下线程接口
创建
pthread_t 就是一个无符号长整型
返回值,成功返回0 ,如果失败的话用返回值的形式来告诉错误码是几,它没有使用errno
一旦调用pthread_create创建线程成功时,新线程转而执行threadRoutine该执行流,从上往下执行如果是死循环那线程永远执行,
如果结束了那新线程也就结束了
而主线程继续向后执行,主线程一开始就有了,main函数是他的入口函数
所以一个进程内会有两个执行流分别执行main函数后续代码,和新线程指向的新代码threadRoutine
因为main函数和指定函数threadRoutine 它在同一块代码被编译时一定使用代码部分不同的地址空间范围,代码区虽然是一整块,但不同函数用的不同的地址空间,注定了这两个线程执行时在代码资源上是分离的。就是说的代码资源分配
这是第三方库不属于c/c++语言,如果不指明用哪一个库,会出现链接错误
那么-I -L怎么不带上呢?
因为pthread库在系统里已经默认安装了,编译器能找到库的路径的,只是不知道要链接哪一个库而已
用ps axj查进程 会发现只有一个进程,这个进程里有两个执行流
我们可以用新命令
ps -aL 查看当前用户启动所有轻量级进程
LWP是什么呢?
Linux中CPU调度基本单位是线程,线程在Linux当中叫做轻量级进程,cpu调度时不仅仅只看pid
,更要看到每一个轻量级进程也要有自己对应的标识符,所以轻量级进程就有了LWP,叫做light weight process id
一个是6631 一个是6632
所以cpu调度执行流是按照LWP来进行调度的
所以CPU根本就不看PID,他看的是LWP,所以你以前是不是讲错了,以前不是按照进程为单位来调度的,每个进程都要有标识符吗?
仔细看上面的执行流它的PID 和LWP 是相同的,证明了:
1 . 上面线程叫做主线程,因为PID == LWP = = 6631 ,最先有的线程是它
2.下面那个线程 PID != LWP 说明他是被创建出来的
OS就能根据LWP和PID是否相等来决定你是否是主线程。
所以以前调度时我们讲错了吗?并没有
以前启动的每一个进程它调度时拿PID和拿LWP是一样的效果
单进程不就是一个进程内只有一个线程
LWP是调度的基本单位,OS调度时看LWP,但是他也能够确定哪一个是主线程,哪一个是新线程
发信号杀掉任意一个线程默认都会导致整个进程被杀掉
那这个信号算是发给线程的还是发给进程的?
我们认为他是发给进程的,因为每一个线程都是进程的执行分支,你发给线程就是发给进程的。
今天我们再写一个show函数,这个函数可以被多个执行流同时执行,那么show函数被重入了哦
今天再定义一个全局变量,所有线程都是共享的。
为什么都能看到,因为它们共享地址空间啊。
所以线程之间要通信很容易,如果定义一个大缓冲区,一个线程写一个线程读那不就俩通信了吗
所以线程中天然两者之间看到的资源共享,为通信方便提供了很好的技术准备
线程的共享性容易实现,他是先进性的表现
如果线程异常了导致进程收到浮点数错误,所以整个进程被干掉了
我现在挺好奇pthread_create形参的那个控制线程的tid,就想打印出来看看
你这个tid 也不是 LWP啊,那你这个tid是啥?
我们把tid的打法改为%p,真像个地址。
所以tid并不是直接是线程的LWP,因为LWP是OS层的概念,只要OS自己知道就行了,
作为用户不关心,我只关心tid,是给用户去使用的
等待
一个新线程一旦被创建出来了,是主线程先跑还是新线程先跑呢?
不确定,调度器说了算
谁应该最后退出呢?
肯定是主线程
为什么老是让他最后退出呢?
谁让你主线程创建了新线程,创建新线程本质就是在对线程做管理
所以你既然要管不把人家新线程管完算什么管理呢?
你不能自己先退了,让人家新线程怎么办。
目前所知,其实主线程要是退了一般代表进程退出了,所以新线程就跟着退
谁先运行不清楚,一定要保证主线程最后退
那我主线程就当甩手掌柜死循环干我自己的事情,为啥不让主线程退因为你得管我新线程
我新线程要是退了,你不管吗
所以新线程退出时也要被等待,如果你主线程不等我,也会造成类似子进程退出父进程不等待
的情况,造成类似于僵尸进程的问题
一句话
新线程被创建一般也要被等待,如果你不等默认会导致类似僵尸进程的问题
这个代码我们没办法验证,因为新线程一退查也查不到了,但确实存在这个问题
更重要的是,为什么要创建一个线程啊?我把新线程创建出来就是为了办事情的。
你把事情办的如何,我怎么知道?还是我不关心了?
所以新线程运行结果数据你是不是也得给我主线程
所以线程等待
1.防止新线程造成内存泄漏
2.如果需要你也可以获取新线程的退出结果
怎么等待呢?
pthread_join 等待一个终止的线程
pthread_t tid 传入你要等待哪个线程
返回值成功返回0,失败返回错误码
线程类函数所有出错码不用errno,统一用返回值返回,他不用全局的
新线程一旦把自己入口函数执行完了,默认线程就退出了
新线程退出不会影响主线程,
主线程等待的时候,默认会阻塞式等待新线程!
现在已经可以用pthread_join保证新线程不退,我主线程也不退,阻塞等待
现在的问题是主线程怎么知道新线程执行完毕的执行结果如何?
一个线程执行完毕的执行结果最终通过返回值让我们知道的。
现在的问题是,你的主线程怎么知道新线程的返回值呢?他们可是两个执行流
你是两个线程,一个主一个新
两个task_struct在内核中
新线程的返回值返回给用户层,主线程在用户层可以通过pthread_join来吧新线程的返回值拿到
不管是pthread_join库函数接口 还是新线程的返回值都属于pthread库的内部!
新线程执行完返回值直接写到pthread库里
你要获取库里面的void* ,用户自己需要定义一个void* 变量,取地址传给形参
join里面第二个形参二级指针void** ,在join里面这个形参解引用,再把库里面新线程返回值赋值给这个形参解引用,上层用户定义的void* 变量就拿到了新线程的返回结果。
有点问题,一个执行流退出结果有三种呢,为什么这里怎么不管线程出异常了?
线程也有可能出异常啊,为什么你join只有退出码不考虑异常呢?
答:它做不到,一旦线程出异常,主线程也就跟这遭殃了join还返回啥呢。
异常问题是进程考虑的。
你线程hold不住,你只考虑退出码这种情况就行
终止
如果在任何一个线程中直接exit( ) ,会直接终止整个进程
exit是用来终止进程的,不能用来终止线程。
主线程要是退了,整个进程也跟着退,你别说线程还没执行完,线程也要退
1、线程函数中直接 进行return ,就代表线程退出
2、pthread_exit( )
谁调用就终止谁,参数就是线程函数返回值,return退出码
3、线程取消pthread_cancel 不常见
前提是线程真的已经创建出来了
形参是要取消哪一个进程 tid
如果一个线程本身是被取消的,该线程退出结果是PTHREAD_CANCEL是宏-1
根据目前知识,线程的大部分资源都是共享的
正文代码其实也是共享的只不过每个线程一人一块代码,单独写个公共方法函数被两个线程都可以读到,函数是随时可以被重入
全局变量也是可以多线程访问的
共享区两个线程都在用cout printf,说明共享区也是被所有线程共享的
栈一定是被所有线程私有的
命令行环境变量今天不考虑
关键在于堆区,重谈线程入口函数参数和返回值
线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!
不要狭隘的认为参数传个字符串,返回值返回个整数
我们现在整个代码都是在堆上new的,发现代码互相交叉式的
在主线程New了一个对象传递给了新线程,在新线程new了一个对象传递给了主线程
说明什么?
说明堆空间也是被线程共享的
这不会出问题吗?一般不会
因为堆空间指针谁拿着指针,谁就访问堆空间
目前我们的原生线程pthread库,属于原生线程库
c++11语言本身也已经支持多线程了 VS 原生线程库 他们两个什么关系?
别和我说什么C++11支持什么多线程,它的底层实现就是封装原生线程库
编译时如果你不加 -lpthread 就找不到这个库
重新理解一下什么叫做原生线程,你刚刚看了pthread_create会产生一个线程ID
这个ID可不是内核级别的LWP那个数字,他们俩不一样
现在我想知道这个线程ID是什么?
另外你还说线程都有自己独立栈结果,你怎么证明?
clone和fork底层原理类似,创建子进程
clone专门用来创建轻量级进程的,我们不用这个接口,看一下复杂的接口参数就知道了
现在的问题是clone我们用不了,有些接口系统不让用,这个接口就被pthread线程库封装了
给我们提供了create , join…这样的接口
上层用户要有线程的概念,也就是线程库
clone允许用户在应用层传入1个回调函数,和1个用户空间来代表轻量级进程在运行过程中:
1.它要执行的代码
2.它在运行过程中形成的临时变量
线程库要封装clone,每一个线程都要给它提供执行方法,线程库内部还要开辟空间
把方法交给clone里面的回调函数,栈也交给它
方法最终暴露出去就是你自己创建线程线程时pthread create 传的回调函数,栈结构你是没管的
也就是说线程的概念是库给我们维护的。
问题:
当你自己执行多线程代码时用原生线程库时你这个库要不要加载到内存中,加载到哪里??
原生线程库可是一个动态库啊
答:
要加载到内存里,别跟我扯什么线程,我就是一个进程
执行时这个库一定被加载到内存中,经过页表把pthread库映射到了共享区里面
不要觉得共享区只有c/c++库
对应pthread库里面,每一个创建好的线程,在库里面就要给我们开辟出一段空间用来充当新线程的栈。
所以线程库要加载到内存,未来所有讨论都是基于内存的!
所以我的线程的ID是多少,我的栈是多大,我线程回调方法地址是什么,我线程时间片是多少?
所有这些字段请问OS关不关心,知不知道?
不知道,因为它没有线程概念
但是这些所有属性,叫做线程的属性
线程的概念是库给我们维护的这句话该怎么理解呢?
线程概念既然是库给我们维护,意味着线程库中要维护线程,主要维护线程的概念,
不用维护线程的执行流!
意思就是说,线程底层就是轻量级进程执行流tast_struct,但是线程相关很多用户关心 的属性
必须由库来维护
线程库里面同时会存在多个被创建的线程很正常,
线程库注定要维护多个线程属性集合。线程库要不要管理这些线程呢??
要,先描述,在组织!
所以每创建一个线程在线程库中就要创建线程库级别的线程控制块
,它包含了线程很多属性,对上为用户提供诸如这个线程回调函数在哪里,独立栈在哪里,
线程的id是多少,
更重要的是线程的lwp指向底层哪个执行流task_struct,用户未来访问线程时,它只要找到线程控制块执行流task_struct自然而然就会由OS底层自动调度它就执行你上层的代码了
你想获取线程的属性你就通过线程控制块获取吧
所以线程是由用户层维护的,他是在OS之上的,称为用户级线程!
用户级线程什么鬼呢?说白了就是由用户把线程的结构体维护起来。
所以再来看这张图
这是某一个PCB的进程地址空间,这是你加载到内存的pthread.so库
库已经加载到内存了,然后被映射到进程地址空间共享区的
你这个进程一把线程,另一个进程也可以一把线程,不要紧,你们都映射同一个库
随着线程创建越来越多,每一个线程在创建时都在库里创建一个线程tcb,
tcb包含了线程很多属性,每个线程都有这个结构,所以在这个库里面把所有tcb按照数组方式给我们维护好,先描述在组织就有了
未来访问线程。比如等待线程,获取线程退出结果,退出结果就是写在它的局部存储里,
你要形成变量时要对数据做压栈,你就用线程栈。
未来你有十个线程,就有10个tcb
为了快速找到每一个tcb在共享库里,所以把一个tcb在内存中起始地址叫做线程的tid!!
为什么要用这个起始地址作为tid呢?
1.它存的是地址
2.它在用户空间
3.是虚拟地址可以直接访问
所以线程你想获取他的属性直接拿着tid,库函数他就直接找到属性了
下面是时候谈 线程栈了
每一个线程运行时一定要有自己独立的栈结构
因为每一个线程都会有自己的调用链
注定了每个线程必须有自己独立调用链所对应的栈帧结构
其中 主线程直接使用地址空间中的栈即可
我们剩下的新线程,首先在共享区线程库里面为新线程创建tcb,
起始地址作为线程tid, 线程控制块包含默认大小一段空间叫做线程栈,
然后要在内核中创建执行流pcb,它就在库中调clone,
把对应线程执行方法和线程栈传递给clone,
所以clone执行流调用时中间形成的临时数据都会压入到这个tcb里面的线程栈
换句话说,所有对应非主线程它的栈都在库中维护,即共享区维护
除了主线程,所有其他线程的独立栈,都在共享区具体来讲是在pthread库中,tid指向的用户tcb中!