调度器

多个线程可以属于同一个进程,并共享内存空间。多线程不需要新的虚拟内存空间,不也不需要内存管理单元处理上下文的切换,无需新建页表。线程的通信也是基于共享内存进行。

虽然线程比较轻量,但调度时也有较大的开销,每个线程会占用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 数据结构

  1. G goroutine 一个待执行的任务
  2. M 操作系统的内核级线程,由操作系统的调度器调度和管理
  3. 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,唤醒后进入自旋态。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐