Go并发编程——调度器
调度器
多个线程可以属于同一个进程,并共享内存空间。多线程不需要新的虚拟内存空间,不也不需要内存管理单元处理上下文的切换,无需新建页表。线程的通信也是基于共享内存进行。
虽然线程比较轻量,但调度时也有较大的开销,每个线程会占用1M以上的内存空间,切换线程会消耗较多内存,恢复寄存器的内容(硬件上下文)还需要向OS申请或销毁资源,每次线程上下文切换都会消耗 1μs左右的时间,但 goroutine 上下文切换约为0.2μs,少了80%的开销。
Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销。
1 设计思想
- 单线程调度器(0.x)全局只有一个 M(内核级线程),任务只能一个个顺序执行,一旦阻塞,后续任务都无法执行。其意义在于建立了 G、M 结构。
- 多线程调度器(1.0)
GOMAXPROCS环境变量控制程序中的最大处理器数。调度器和锁是全局资源,锁竞争严重。线程需要经常互相传递 Goroutine,有大量延迟。 - 任务窃取调度器(1.1)引入了 P,作为 G-M 的中间层。在 P 的基础上实现基于Work Stealing 的调度器,P 本地队列为空时,会从其他 P 的队列中随机窃取一半 Goroutine,大大提高 M 的利用率。
- 抢占式调度器(1.2~至今)早期只有读写、主动让渡、被动阻塞时才会触发调度切换,当GC进行STW时,若一个 G 在阻塞调用,GC会一直等待
- 基于协作的抢占(1.2~1.13)
- 基于信号的抢占(1.14~至今)使用操作系统信号(SIGURG)异步抢占
2 数据结构
- G goroutine 一个待执行的任务
- M 操作系统的内核级线程,由操作系统的调度器调度和管理
- P 处理器,可以看作运行在线程上的本地调度器
2.1 G
goroutine 是Go语言在用户态提供的轻量级线程,粒度更细的资源调度单元,能高效利用机器的CPU。
核心特征
- 轻量:2KB的初始栈空间
- 低上下文切换开销:用户态下完成调度,不涉及内核,并且只有少数几个硬件上下文
生命周期
Goroutine 在其生命周期中会经历多种状态变换,主要包括:
_Gidle:刚分配,尚未初始化_Grunnable:在运行队列中,等待被执行_Grunning:正在执行_Gsyscall:正在执行系统调用_Gwaiting:阻塞等待(如等待锁、Channel 等)_Gdead:已退出
2.2 M
代表操作系统线程。
核心特征
- M 和 P 绑定
- M 的数量最多为 10000
sched.maxcound = 10000,默认情况下创建CPU核心数个 M - 最多只有
GOMAXPROCS个活跃线程能够正常运行
状态
- 自旋:正在寻找可运行的 G
- 非自旋:找不到,进入休眠,等待被唤醒
数据结构
type m struct {
g0 *g
curg *g
...
}
- g0 是持有调度栈的 goroutine,一个比较特殊的 goroutine。会深度参与调度过程,包括 goroutine 的创建、大内存分配和 CGO 的执行。
- curg 是当前在线程上运行的 goroutine
2.3 P
通过处理器P的调度,每一个内核线程都能执行多个 goroutine,并在其阻塞时,及时让出计算资源,提高 M 的利用率。
- P 是 M 和 G 之间的调度中间层,提供线程需要的上下文环境
- 每个 P 有一个本地运行队列(LRQ) ,存储待执行的 G
- P 负责将 G 分配给 M 来执行
- P 的数量由
GOMAXPROCS决定,默认等于 CPU 核心数 - 每个 P 最多关联 256 个 G
启动时,创建 GOMAXPROCS 个P,并绑定到不同的内核线程上。
状态
| 状态 | 描述 | |
|---|---|---|
_Pidle |
处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空 | |
_Prunning |
被线程 M 持有,并且正在执行用户代码或者调度器 | |
_Psyscall |
没有执行用户代码,当前线程陷入系统调用 | |
_Pgcstop |
被线程 M 持有,当前处理器由于垃圾回收被停止 | 、 |
_Pdead |
当前处理器已经不被使用 |
3 调度器启动
-
初始化调度器
-
初始化锁、堆栈和内存分配器
-
初始化系统线程 M0
-
设置 Go环境变量和命令行参数
-
初始化GC
-
-
创建 main goroutine
-
启动调度循环
go func() 时
- 获取当前 G 的 M 和 P
- 创建一个 g 结构体
- 初始化栈空间和上下文信息
- 优先放入 P 的本地队列 —— 本地队列满,一半放入全局队列
- 设置为下一个要执行的 G
调度信息
- 栈指针
- 程序计数器
- 上下文
4 调度循环
schedule() → findrunnable() → execute() → gogo() → Goroutine 任务 → goexit() → goexit0() → schedule()
| 函数 | 职责 |
|---|---|
schedule |
调度入口,选择一个可执行的 G |
findrunnable |
查找可运行的 G(本地队列 → 全局队列 → 工作窃取 → 网络轮询器) |
execute |
准备执行 G,切换到 G 的栈 |
gogo |
汇编实现,真正切换到 G 执行 |
goexit / goexit0 |
G 执行完毕后的清理工作,将 G 放回空闲队 |
5 触发调度
协作式调度为主,抢占式调度为辅
主动挂起
- 主动让出 CPU 给其他 G
- 阻塞操作:如 channel读写
- 状态
_Gwating
系统调用
- M 因系统调用陷入僵直态,与P解绑,P寻找其他空闲M
- 系统调用返回后,M尝试重新获取一个 P,没有则G被放入全局队列
抢占式调用
- 系统监控检测到 G 的运行时间过长
- 发送抢占信号
- G 暂停执行,放回队列
6 线程管理
缺少空闲 M 时,调度器会创建新的 M,尝试与一个空闲 P 绑定,没有绑定 P 的 M 进入阻塞。
自旋线程(有 P 无 G)是为了快速响应新任务。
找不到可运行的 G 时,进入非自旋状态,进入休眠,有新 G 要执行时,唤醒 M,唤醒后进入自旋态。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)