【Linux下】线程概念
理解线程
一般的书上都是这么描述线程的
线程:是在进程内部运行的一个执行分支,属于进程的一部分,粒度要比进程更加细和轻量化
从上面我们就可以知道,进程是可能存在多个线程,即进程比线程可能是1:n的关系;所以os肯定是要管理线程的,怎么管理呢?
常规os的做法:先描述再组织,例如windows
即windows下os管理线程实际上是像Linux下管理进程一样,为线程设计专门的线程描述块(TCB), 而后使用数据结构将所有线程串起来进行管理
如下图:
而Linux下:选择了复用进程描述块PCB描诉线程,即使用管理进程的方式管理线程。
这样做的好处是:
- 不用再专门为线程设计线程描述块,各种算法,并且维护线程和进程的复杂的关系(处理TCB和PCB的复杂关系)
- os只需要聚焦于线程之间的资源分配上即可
线程使用的也是PCB描述块,那么Linux下的进程到底是怎么样的呢?之前的博客说的并不完整,因为没有涉及到线程相关的知识
重新理解Linux下线程和进程
我们在之前博客所讨论的进程是这样的
- 进程中只有一个TCB 描述块 --即进程中只有一个执行流
实际上LInux下的进程
- 含有多个TCB描述块 – 即进程含有多个执行流
然后再重新理解一下CPU眼中的PCB描述块
我们之前曾说,CPU调度进程,实际上将一个一个PCB放到自己的runqueue里面,之前我们是将一个PCB看成一个进程,而现在我们知道实际上的PCB要小于我们之前所说的PCB的粒度,即实际上CPU眼中,一个PCB就是一个等待调度的执行流–线程(可能属于不同的进程)
即在os视角下的进程和线程之间的关系是这样的
- 进程是承担分配os资源的基本实体
- 线程是CPU调度的基本单位,承担进程部分资源的基本实体–进程划分资源给线程
如图:
这样我们就可以重新理解一下一开始说的线程的概念了
- 线程:是在进程内部运行的一个执行分支,属于进程的一部分,粒度要比进程更加细和轻量化
理解线程是在进程运行的
Linux下的线程是在进程的地址空间下运行的,不同的线程可能运行进程中的不同代码和数据
理解线程是一个执行分支
CPU调度时,只看到线程是一个PCB结构,而每个线程都是被委派过对应的代码和数据的(可以单独执行),CPU眼中一个PCB就是一个可调度的执行流,所以CPU可以直接调度线程
理解LInux下的进程也被称为轻量级进程
Linux下,使用进程描述块task_struct 来描述线程,即相对于其他系统实现来说,一个PCB可能就代表一个进程,而在Linux下,可能描述的是一个进程(只有一个PCB的进程) ,也可能是一个线程,所以说Linux下的,进程更加轻量化,本质上是因为,就于管理机制这部分,os对进程和线程并没有做过多概念上的区分
线程操作接口
因为Linux下对进程和线程管理机制,本质上没有对进程和线程做太大区分,Linux只给我们提供了在同一个地址空间内创建PCB的接口–创建线程,以及分配资源给指定的PCB的接口–分配资源给线程,即Linux本身并未提供像操作进程的接口给我们操作线程;(补充:实际上,Linux也可以为我们提供线程操作的系统级别接口,但为了LInux中的代码不会太过于臃肿和赘余,就将这部分工作交给了我们用户层)
而如果使用原生接口,实际上对用户是极其不友好的,例如:我们要自己写分配资源的逻辑,以及回收线程资源的逻辑…等部分代码
于是就有一些系统级别的工程师站了出来,对原生接口进行了封装,于是线程库就产生了(属于用户层),于是我们引进该库之后,就可以像操作进程一样操作线程了,而我们之后对线程控制的函数都是依据该线程库展开的
线程和进程
线程组里的线程共享的资源
- 文件描述符表
- 各种信号的处理方式
- 当前工作目录
- 用户组id和组id
- 同一地址空间(即代码和数据)
线程所独享的资源
- 线程id
- 一组寄存器(上下文数据)–保存线程执行的临时数据
- 栈 --因为线程是一个单独的执行流,所以线程肯定是有自己的栈结构的
- Errno
- 信号屏蔽字
- 调度优先级
线程和进程的关系
例子:就好像一个国家,分配国家资源的基本实体就是一个家庭,一个家庭里面可能会有不同的人,不同的角色,但一家人都是为了让家庭过得更好而努力,互帮互助,共享家里面的资源,也独享在家里面的部分资源,例如自己的卧室等
os就像一个国家,而进程就好像上面的家庭,线程就是家庭里面的成员
代码验证
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define NUM 5
//测试线程创建和线程退出
void* pthread_run(void *args)
{
int id = *(int *)args;
while(1)
{
printf("I am 新线程[%d]:%p\n",id,pthread_self());
sleep(1);
}
return (void*)123;
}
int main()
{
pthread_t tid[NUM];
for(int i=0;i<NUM;i++)
{
pthread_create(&tid[i], NULL, pthread_run, (void *)&i);
}
while (1)
{
printf("I am main线程:%d\n", getpid());
sleep(1);
}
return 0;
}
实验现象:
我们发现,竟然同时有6个执行流同时在向显示器打印,这在之前的单进程程序是不可能实现的,虽然说打印时有些凌乱(因为线程是同时向显示器打印的,我们目前还未控制线程访问显示器文件的次序)
查看线程的命令
ps -aL
例:使用ps -L查看上面的程序里的线程,我们会发现主线程的线程id和进程的pid是一样的,我们后面会提及线程组的概念,实际上进程pid也叫线程组id
在这还需要再说明一个,我们之前曾经说,CPU调度进程PCB时是通过PID进行区分的,而实际上CPU调度PCB是通过线程id–LWP,进行调度的,因为线程是CPU调度的基本实体;而我们之前说CPU调度进程是是通过PID进行区分的,其实也没有错误,因为我们之前讨论的都是单线程的情况,即进程PID=主线程LWP的(如上图进程pid 和主线程的LWP 都为23243),
线程优点
- 创建一个新线程的代价要比创建一个新进程小得多
创建一个进程,需要在内核中重新申请(task_struct mm_struct files_struct 页表…等等资源),也就是说,创建进程实际上是一个从0到1的过程;而创建一个线程,mm_struct ,files_struct…这些资源都是直接使用其所属的进程的
- 于进程之间的切换相比,线程之间的切换操作需要操作系统做的工作要少得多
切换的临时数据少,且因为线程之间可能是使用同一地址空间的,就是说访问的页表可能不需要重新改变
- 线程所占用的资源要比进程少得多
- 能充分使用多处理器的可并行数量
- 在等待慢速i/o操作结束的同时,程序可执行其他的计算任务
3,4 条实际上多进程也具备这个优点
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现计算
- i/o密集型应用,为了提高性能,可以将i/o等待的时间重叠,线程可以同时等待不同的i/o操作
而“密集”到什么程度呢? 线程是否是越多越好呢?
并不是,对于计算密集型来说,如果线程太多,就会导致线程的过度切换(100个线程同时在run_queue里面等待CPU调度),整体运行速度依旧还是会很慢
对于i/o密集型应用来说,虽然说线程数虽然可以多一点–因为i/o主要时间都是线程在等待i/o资源就绪,就没有这么多的线程在等待CPU调度,但如果太多了,也会产生和计算密集型应用一样的困扰
所以线程的优点有如下:
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠(主要是等待的时间)。线程可以同时等待不同的I/O操作。
线程缺点
性能损失:
- 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
- 理解: 因为一个计算机密集型线程,是一直保持在CPU的runqueue中进行等待CPU调度的,而不像其他线程可能大部分时间在等待某些资源就绪(在wait_queue里)
健壮性降低
- 理解:我们知道进程和进程之间是存在独立性的,所以一个进程挂掉,并不会影响其他进程的正常工作;而线程和线程之间,是存在共享资源的情况的,而当一个线程组中的任意一个线程出现程序异常时,整个线程组都会崩溃掉。–因为线程程序异常,os是给线程所在的进程发送的信号,而后将整个进程终止掉,进程都不在了,里面的线程怎么可能还存在
缺乏控制:
- 因为线程之间可能存在**共享资源(共享程序地址空间)**的关系,而且一个线程是可以做到访问其他线程的代码和数据的,这样就可能会出现问题
编程难度提高:
- 编程和调试一个多线程代码比调试一个多进程的代码要难得多
因为一个线程可能在任何时刻修改了其他线程的代码和数据,而造成程序异常的问题的出现
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
例如:百度网盘实现的边下边播功能