目录
1、线程初识
1.1线程的概念
线程是进程内部的一个执行分支,线程是CPU调度的基本单位
那什么是进程呢?
我们之前学习了解到的进程是加载到内存中的程序
进程 = 内核数据结构 + 进程代码和数据。
今天我们要推翻这一观点,该观点是片面的,不正确的!
我们之前认为的task_struct就是进程,其实这一个个的task_struct是我们的进程的执行流!!!那进程究竟是什么呢?
进程其实是包含文件描述符表,pcb,页表等等,上面框的一整套才是真正的进程!
1.2.关于线程和进程的进一步理解
我们要抛弃之前的想法,进程不仅仅是PCB数据结构。而是一整套资源的时候,我们就应该清楚进程创建成本很高。原因就是:创建进程时还需要构建文件描述符,信号表,PCB,页表,等等这就会造成空间和时间的浪费。
从内核来看:进程本质上是是一个容器,承担分配系统资源的基本实体,包括执行流资源、地址空间资源、页表映射关系,代码和数据这一整套的资源。
线程只是进程当中的一个执行分支~
1.3.线程的设计理念
线程我们一般称为tcb(进程是pcb),对于线程来说,也一定要和进程一样需要对应操作方法:新建,暂停 ,销毁,调度。那我们如何对线程进行这些操作呢?
如果我们要设计线程,OS也要对线程进行管理。(先描述,再组织)
Linux的设计者认为,进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和算法,直接复用代码,所以Linux是用进程模拟的线程!
1.4.进程vs线程(图解)
横向的是进程,纵向的是执行流。我们以前讲的进程,是今天讲的特殊情况
1.5地址空间的第四谈
如何从页表定位到物理地址?
页目录保存的是二级页表的地址
以后我们在查页表的时候,先拿虚拟地址中的前10位,查页目录,选择具体哪一个页表,然后再查询虚拟地址的中间10位就能找到页框,就肯定能找到一个物理地址,最后我们拿着虚拟地址的最后12位,就是页内偏移,因此就直接找到物理内存中的地址了,有页框的物理初始地址,后面还有虚拟地址后12位的页内偏移就可以定位一个物理地址了!
页表里面存储的是页框的物理地址。
给不同的线程分配不同的区域,本质就是让不同的线程,各自看到全部页表的子集!
2.线程的控制:
2.1.关于线程控制的前置知识
在学习之前,我们要清楚关于创建新线程的前置知识——POSIX线程库。
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项(makefile文件要加上-lpthread”选项)
用户层面:用户知道“轻量级进程”这个概念吗?并没有,只有线程和进程,
系统层面:将轻量级进程的系统调用进行封装,转成线程相关的接口提供给用户
内核层面:Linux到底有没有真线程呢?没有,Linux只有轻量级进程。Linux系统,不会有线程相关的系统调用,只有轻量级进程的系统调用
新线程和主线程,谁先运行呢?不确定,由调度器决定
2.2创建线程的系统调用:
这个几号手册具体代表的什么含义?
这个3就是代表他在3号手册中
这种几号手册,一般来说,1号手册都是一些命令,2号手册都是系统调用,3号手册就是C库函数。就是分门别类放置不同的东西。
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 新线程
void *threadStart(void *args)
{
while (true)
{
std::cout << "new thread running..." << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadStart, (void *)"sthread-ew");
// 主线程
while (true)
{
sleep(1);
std::cout << "main thread running..." << std::endl;
}
return 0;
}
当我们进行make编译的时候,会报错:(.text+0x1b): undefined reference to main
线程未定义,之所以会出错是因为Linux下使用线程需要引用线程库。必须在makefile文件中加上-lpthread,因为线程库里面是函数。编译器并不认识,不是C语言/C++自带的,而是Linux自己创建的原生线程库文件
再加上线程库之后,就可以正常运行了。
可以看见,主线程和新线程是可以同时运行的!并且主线程和新线程用的是同一个pid。
所以这两个虽然都是不同的执行流,但是是属于同一个进程内部的,我们可以使用ps -al来查看不同的线程信息
这个pid是对应进程的pid,这个LWP其实就是这个线程的id!!!
2.3.线程终止
同一个进程内的线程,大部分的资源都是共享的,地址空间是共享的!
主线程退出 == 进程退出 == 所有线程都要退出
多线程代码往往健壮性不好。进程之间是独立的,不能共享资源哦。但是线程可以共享资源哦!
我们怎么没有像进程一样获取线程退出的退出信号呢?
因为线程出异常了,会将整个进程退出,根本没有机会读到pthread_read的退出信号,因此该函数并不考虑退出信息。但是父进程会接受子进程退出时的异常信息
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
不能用exit终止线程,因为他是终止进程的!
pthread_ exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_ cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
2.4.线程等待:
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。不然也会造成内存泄露问题!
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
2.5. 分离线程
分离线程的概念:
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。这就是分离线程
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
被分离的线程不需要join,虽然线程已经被分离了,但是资源还是要共享,所以当主线程退出时,被分离的线程也是要退出的,底层也是同一个进程!只是不需要退出了。我们希望主线程是最后一个退出的。所以一般程序的主线程永远不会退出,是永驻进程!
3.关于进程和线程比较的深层次问题
3.1已经有多进程了,为什么还要有多线程??
创建一个进程需要创建PCB,地址空间,页表,加载代码与数据,创建文件缓冲区等很多操作,但创建一个线程,只需要创建一个PCB,复用原本的地址空间。创建进程的成本比创建线程高很多!切换进程时不仅仅要更换上下文数据,更换地址空间等很多操作,切换线程只需要切换PCB!!!线程删除成本也很低。
但是线程也有缺陷:一个线程出错(野指针)就是这个进程出错了,因为他们使用同一个地址空间,所以其他的线程也会报错退出!!! 线程的健壮性很差!而进程是独立的互不影响!进程和线程各有特长!
3.2线程的切换vs进程的切换
CPU里面的cache会将后续代码提前缓存到cache(默认会从后续代码编译,如果调用函数跳转代码就会读取失败,也就是cache命中失败,但命中失败的概率较低),这样就不用一直向内存中提取内容,大大提高了CPU寻址的效率,所以以后CPU读取数据的时候,就不需要从内存中读取数据了,直接从CPU里面的cache里面读取
所以进程切换不仅仅要考虑到寄存器之间的变化,但也要考虑cache的切换,cache可能只对上一个进程有效,切换进程之后,就会丢弃,重新寻址,这部分要消耗的成本很大
但是如果是线程切换,寄存器、上下文代码同样也要变化,但是因为是线程,共享同一份代码所以cache不需要更换,因此就减少了很多的成本。
因此线程切换要比进程切换更简单,消耗的成本更低!
4.总结:
4.1.线程的优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现I
- /O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
4.2.线程的缺点:
- 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
4.3.线程异常:
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出