这里写目录标题
- 线程概念
- 什么是线程
- 简介
- 图解
- 内核原理
- 图解
- 线程共享资源与非共享资源
- 共享资源
- 非共享资源
- 线程优缺点
- 线程控制原语
- pthread_self、pthread_create
- 简介
- 代码
- 总结
- 循环创建多个子线程
- 错误代码
- 线程间全局变量共享
- pthread_exit
- 简介
- 代码
- pthread_join(回收线程)
- 简介
- 代码
- pthread_cancel
- 简介
- 代码
- 一级目录
- 二级目录
- 二级目录
- 二级目录
- 一级目录
- 二级目录
- 二级目录
- 二级目录
- 一级目录
- 二级目录
- 二级目录
- 二级目录
线程概念
什么是线程
简介
图解
对于一个进程来说,他有独立的进程地址空间,如上图左侧0~4G的进程地址空间,且他有一个PCB进程控制块
而当该进程调用pthread_create()创建线程时,这个线程不会再有新的0~4G的进程地址空间,而是使用当前进程的地址空间,但是该线程会创建出一个自己的PCB控制块
而一旦一个进程创建出一个线程,该进程也叫做线程了,就相当于从一开始的整租->合租,虽然你是第一个租的,但由于有别人一起来租,你们俩就都叫合租
所以,进程是最小的内存的资源分配单位,而进程是最小的执行单位,可以把进程看做是一个只有一个线程的进程
关于线程影响单核CPU分配资源:
起初,我们有三个进程,这三个进程一开始是平等的,他们会平均的争夺CPU,而此时,如果A进程新建两个线程,一共三个线程,此时CPU就会把更多的时间片分给进程A,因为最小执行单元是线程而不是进程,所以,在CPU眼里,进程A的三个线程就是三个进程,他将平均的时间分配给进程A的三个线程时,就意味着把更多的时间片分配给了进程A
但是不要向一个进程内塞过多的线程,这样反而到达一定的峰值之后,物极必反
如下图,firefox的例子:
我们开启一个firefox时,可以使用ps -Lf pid 来查看当前进程的线程状态,可以看到,启动一次火狐,他就开了45个线程(利用线程池的原理),其中LWP列的编号为线程号,可以看到,线程号是继续上面的进程号的,所以,在CPU眼里,线程是最小的执行单元
总结:
内核原理
图解
先看右边,当进程地址空间向物理内存映射时,他的数据段与物理内存并不是直接通过MMU去映射,而是先映射到PCB中的一个“描述虚拟地址空间”的指针,之后再映射到MMU,然后再映射到物理内存地址
而右图中圆圈圈起来的那一步:即从PCB到物理内存的具体映射,看左边所示:
PCB有一根指针,之后该指针指向一个页面,页面指向页表,页表指向页目录,页目录就会映射物理地址
而此时如果创建一个线程,那么该线程就会创建出一个PCB,指向同样的页面,所以,一个进程中的线程的地址空间是共享的
而这里区分新创建的一个进程:
如果创建一进程而不是线程,那么此时就会被分配一块独立的资源,此时该进程的三级页表与之前进程的三级页表完全不同
总结:
线程共享资源与非共享资源
共享资源
1、文件描述符表共享
2、信号的处理方式共享,但是不同线程之间的mask是不共享的,因此可以利用这一点进行指定哪个线程接收信号
5、共享内存地址空间,但是不共享栈区(因为线程就是寄存器和栈的集合,每个线程有每个线程独立的栈空间)
非共享资源
2、处理器线程和内核栈:其实就是寄存器的值和内核的一个栈区
3、用户空间栈也不共享
所以,不管是内核还是用户空间栈,都不共享
4、errno,起初,errno变量是一个全局变量,但是对于线程来说,errno是不共享的,但是对于其他全局变量是共享的
5、信号屏蔽字mask
线程优缺点
对于优点的第三点:数据通信方便、共享数据方便,主要是由于不同的线程是在同一块资源地址空间内的,所以,对于数据来讲,除了栈不共享,其他区域数据都共享(全局变量区、堆区、…)
总结:
线程控制原语
pthread_self、pthread_create
简介
代码
我们称main函数中的线程是主线程
而之后创建的线程是子线程
在主线程中,我们使用pthread_create创建子线程,参数:
参数一:传出参数,表示所创建的子线程的线程ID(注意不是lwp中的线程号)
参数二:传入参数,表示线程属性,通常传NULL,表示使用线程的默认属性
参数三:线程运行函数,该函数返回值必须为void *,参数是一个指针
参数四:表示线程运行函数的参数,由于是使用泛型指针,所以传参时可以传单参,也可以多参(通过结构体指针)
这里我们先传NULL
返回值:成功:0,失败:返回errno(直接返回errno,而不是设置errno)
在线程运行函数中,同样的,我们打印线程的pid和tid
注意:
1、打印tid时,要使用%lu控制符,因为pthread_t是unsigned long类型
2、编译时,要加上-pthread选项,或者加上-lpthread(前面加小L),这是链接线程库
3、main函数创建完子线程之后,不能立马退出,因为所创建的线程要依赖于主线程的内存资源,主线程若结束,子线程的内存资源也就没了,所以可以让主线程sleep,晚结束一会儿,等子线程打印完之后,再结束,而在实习时,使用的是while,保证主线程不结束
4、子线程函数规定返回值必须为void *,所以我们return NULL
总结
循环创建多个子线程
错误代码
可以看到thread的编号都变成了6
错误分析:
由于线程是不共享栈的,所以,上面的代码中,我们取i的地址,而i是main函数的局部变量,所以,我们将i的地址传给线程函数后,线程函数会拿到一个main中局部变量的地址,解引用且拷贝其值,但是在线程解引用拷贝值的时候,i 已经 ++了,因为main中的for循环是一个无需任何资源介入,执行很快的动作,而线程调用需要进出内核,所以,在线程解引用拷贝值的时候,i 已经 ++了,甚至++了很多次
修改:
直接将值,转为void *,之后在线程函数中,再转回int
在64位系统中,int是4字节,指针是8字节,我们先将一个4字节的值给到一个8字节的变量存储,之后,再从这个8字节的变量强转为4字节的变量中存储,只要进的时候不会缺失数据,出的时候也不会,所以,是可行的,但是编译器可能会给出警告,但是是可以运行的
效果:
如果想要消除警告:
总结:
线程间全局变量共享
首先,主线程打印一下var变量的初始值
之后,创建子线程,在子线程中对全局变量进行修改,并打印,之后,主线程停一秒,目的是等待子线程执行完毕,最后,在主线程中打印一下当前var的值
效果:
pthread_exit
简介
最后一句话说的是不要返回局部变量的地址
代码
需求:假如说,我们想要第三个线程退出,而其他线程继续工作
方式一:
使用exit(0),表示正常退出
效果:
第3个以及后面的线程没有输出,这是因为exit表示的是进程退出,整个进程被退出的话,所有的线程也会被退出
而exit之前,已经有两个线程输出,exit之后,所有线程退出,不再会有输出
方式二:
使用return
效果:
可以实现
方式三:
使用带有return NULL的一个函数,显然是不行的,因为return只返回当前函数,返回到上一级,上图return的功能只能影响到func函数,无法影响到tfn函数
效果:
修正:
但是如果所调用的函数中含有pthread_exit(),那么就可以影响到外层的线程,让线程退出,且仅退出线程,而不退出进程
效果:
当然我们直接将其放到线程函数才是最常规的:
补充:
现在来看我们之前的一个问题,就是主线程只创建一个子线程,但是,如果主线程不sleep,会在子线程执行打印之前,主线程return,将main函数返回给调用者,就会把进程结束,从而子线程无法打印内容
现在,我们将sleep(1)去除,同时将return改为pthread_exit((void *)0),这样,主线程退出不再使用进程退出,而是使用线程退出,那此时主线程退出就不会把进程退出,那么子线程也可以继续执行自己的内容了
*注意要将整数0转为void ,或者直接退出NULL也可
效果:
主线程和子线程都打印了自己的内容,当所有线程结束时,进程也就结束了
总结:
pthread_join(回收线程)
简介
功能对标进程的话就是回收线程,类似于wait
原型:
参数一:进程ID,注意这里不是指针,而是值(区别于pthread_create)
参数二:传出参数,表示退出状态,
返回值:成功:0,失败:返回错误号
对比记忆:
对于进程来说,其进程退出的返回值是int,那么要传出其退出状态,就要用int *
那么对于线程来说,其线程退出的返回值是void 星,那么要传出其退出状态,就要用void 星星
所谓退出状态,实际上就是接收return 返回的数据
代码
在子线程中,开辟堆区数据,之后对其赋值,然后返回给主线程的pthread_join,返回值可以使用return ,也可以使用pthread_exit
在主线程中,创建出来子线程之后,使用join阻塞等待回收子线程,并拿到子线程的返回值,主要通过参数二来拿到子线程的返回值,所以这里join的参数二是一个传出参数,要注意其类型要与返回值类型对应,更具体的说,参数二要传入子线程返回值类型的地址,并转为void **
最后返回值会传到retuval指针中,我们操作指针即可
补充:线程返回值是一个数
由于返回值直接将74转为了void * ,也就是直接将74存在了指针变量里,而join的参数二仍然要求是返回值的指针类型,即返回类型的基础上再加一个指针
但是最后打印的时候,要记得retval存放的就是74,而不是74的地址,所以,直接打印retval即可
补充:局部变量不可行:
当子线程想要返回一个自己函数内的局部变量时,肯定是不行的,因为一旦离开子线程,该变量就会被释放,数据就会无效
补充:局部变量可行:
而如果是主线程的局部变量将地址传入子线程函数,在子线程函数中处理完之后再返回出来,那么就可以拿到,这样是没问题的
pthread_cancel
简介
代码
子线程中,循环打印,表示自己还活着
主线程创建出来子线程之后,等待5秒,之后就调用pthread_cancel(tid),将子线程杀掉,这个就相当于进程中的kill
效果: