操作系统 --进程补充
线程是一个基本的CPU执行单元,也是程序执行流的最小单位。引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ视频、文字聊天、传文件)
1. 用户级线程
用户级线程是由用户态线程库创建、调度和管理的线程,内核对其不可见,内核只知道进程的存在。
常见线程库:
- 早期 POSIX 线程实现
- 协程(coroutine)
- Go 的 goroutine(本质是用户级调度)
从内核视角看,只能看到进程A, 而从用户视角看可以看到进程A,以及下面的线程1, 2, 3…
2. 内核级线程
内核级线程是由操作系统内核直接创建、调度和管理的线程,每个线程都是内核调度的基本单位。
操作系统是可以明确感知内核级线程的存在的,实际上再linux系统里面,内核级线程是轻量的进程,都是用clone出来的,只是分配的资源少?
3. 二者比较
用户级线程: 开销小,但是由于操作系统感知不到他的存在,所以一旦一个线程阻塞,那么整个进程内其他线程都会阻塞,(一般情况下涉及到内核态完成的功能必须由内核级线程完成,所以一般用户级线程会有内核级线程与其配合,传统一般是N:1模型,也就是N和用户级线程对应1个内核级线程),所以用户级线程非常适用于
- 计算密集型
- 少I/O切换
- 高并发调度
- 少系统调用
的场景,
现实的场景: Java的虚拟线程, Go的goroutine, 但是Go由GMP模型,不是简单传统的N : 1用户级线程,本质是M : N的用户级别线程,详细看下面理解
内核级线程开销大,但是由于操作系统可以感知,所以不会出现阻塞问题,适用于
- I / O 密集型
- 需要频繁阻塞系统调度
- 需要利用多核CPU
典型的例子:
数据库,桌面应用,Web服务器(大量的IO操作)
Go的GMP
Go的goroutine本质是用户级线程,但采用了类似M : N的GMP模型。
GMP:
Go 的 goroutine 本质是用户级线程,但采用了 M:N 的 G-P-M 调度模型。
G 是 goroutine,P 是调度资源和本地任务队列,负责管理多个 G;M 是 Go runtime 管理的 OS 线程(内核线程),作为 G 的实际执行载体。
每个 M 拥有一个 G0 作为调度和系统调用的执行上下文,多个 G 会轮流运行在同一个 M 上;当 G 发起系统调用导致 M 阻塞时,runtime 会将 P 从该 M 上剥离并交给新的 M,保证其他 G 继续执行。
调度逻辑主要位于 runtime/proc.go 中,通过 schedule → findRunnable → execute 流程完成,findRunnable 按本地 runq、全局 runq、netpoll、work stealing 的顺序查找可运行的 G,从而保持各 P 之间负载相对均衡。
P的运行机制:
源码位置:
runtime/src/proc.go
看的顺序:1
2
3
4
5
6schedule() // 主调度循环
findRunnable() // 找可运行的 G
execute() // 切换并执行 G
goexit() // G 结束
entersyscall() // 进入系统调用
exitsyscall() // 退出系统调用
schedule: Go的心跳:找个可以运行的G,然后运行G
findRunnable: schedule的第一步,找到可以运行的G,顺序1
2
3
41. 本地 P.runq
2. 全局 runq
3. 网络轮询(netpoll)
4. 从其他 P 偷取(work stealing)
这里第四步会使得多个P的任务始终保持相对平衡的数量
