G: 表示
goroutine
M: 表示 操作系统的线程
P: 表示处理器
,运行在线程上的本地调度器
本文主要研究一下 GMP
的内部实现,相关文件目录为 $GOROOT/src/runtime
,笔者的 Go 版本为 go1.19 linux/amd64
。
G
M
P
相关的数据结构定义,全部定义在 $GOROOT/src/runtime/runtime2.go
文件中。
goroutine 只存在于 Go 语言的运行时,是 Go 语言在用户态提供的线程,但是内存占用和上下文切换开销更少,同时启动速度更快。作为一种粒度更细的资源调度单元,能够在高并发的场景下更高效地利用机器的 CPU 资源。
stack
对象表示 goroutine
执行栈内存范围,栈的上下边界分别是 [lo, hi), 两侧没有隐式的数据结构 (有些运行时对象会有隐式数据结构)。
type stack struct {
lo uintptr
hi uintptr
}
goroutine
的运行时表示。
type g struct {
// stack 描述了栈的实际上下边界 [stack.lo, stack.hi)
// stackguard0 是在 Go 栈增长 prologue 中用来和 sp 寄存器做比较
// 正常情况下,stackguard0 = stack.lo+StackGuard, 但是可以用 StackPreempt 触发抢占
// stackguard1 是在 C 栈增长 prologue 中用来和 sp 寄存器做比较
// 在 g0 和 gsignal 栈上,stackguard1 = stack.lo+StackGuard
// 在其他栈上,stackguard1 = ~0 (按 0 取反), 触发 morestack 调用(并 crash)
stack stack
stackguard0 uintptr
stackguard1 uintptr
_panic *_panic // _panic 链表头节点
_defer *_defer // _defer 链表头节点
m *m // 当前关联的 m (线程)
sched gobuf // goroutine 调度相关数据
...
atomicstatus uint32 // goroutine 状态
stackLock uint32 // sigprof/scang lock
goid int64 // goroutine ID (对应用层不可见,但是可以通过其他方法获取到,详情见之前的文章)
...
preempt bool // 抢占信号
preemptStop bool // 抢占时将状态修改成 _Gpreempted
preemptShrink bool // 在同步安全的临界区收缩栈
...
}
sudog
对象表示等待队列里面的 goroutine
对象, 比如向 channel
发送/接收数据时。
sudog
对象主要作为一层中间抽象层,因为 goroutine
和同步对象之间是多对多关系,一个 goroutine
可能在多个等待队列中,可以有多个 sudog
,
同时,多个 goroutine
也可能在等待同一个同步对象,一个对象可以有多个 sudog
。
为了提升程序的运行时性能,sudog
对象从一个特殊的对象池中分配,调用 acquireSudog
函数分配,releaseSudog
函数归还。
type sudog struct {
g *g
next *sudog
prev *sudog
acquiretime int64
releasetime int64
ticket uint32
// isSelect 表示一个 g 是否正处于 select
isSelect bool
// 如果 goroutine 因为 channel c 传递值被唤醒,success 的值为 true
// 如果 goroutine 因为 channel c 关闭被唤醒,success 的值为 false
success bool
...
}
gobuf
对象表示 goroutine
的运行现场表示,该对象在调度器保存数据或者恢复上下文的时候用到,sp
和 pc
寄存器字段用来存储或者恢复寄存器中的值,改变程序即将执行的代码。
type gobuf struct {
sp uintptr // sp 寄存器
pc uintptr // pc 寄存器
g guintptr // goroutine 对象指针
ret uintptr // 系统调用返回值
lr uintptr // arm 上用的寄存器,amd64 忽略
}
goroutine
的状态列表,最常见是 _Grunnable
, _Grunning
, _Gwaiting
。
const (
// goroutine 刚被分配并且还没有被初始化
_Gidle = iota // 0
// goroutine 处于运行队列中,没有在执行代码,没有栈的所有权
_Grunnable // 1
// goroutine 可以执行代码并且拥有有栈的所有权,M 和 P 已经设置并且有效
_Grunning // 2
// goroutine 正在执行系统调用,没有在执行代码,拥有栈的所有权但是不在运行队列中,此外,M 已经设置
_Gsyscall // 3
// goroutine 处于阻塞中,没有在执行代码并且不在运行队列中,但是可能存在于 Channel 的等待队列上
_Gwaiting // 4
// 没有使用这个状态,但是被硬编码到了 gbd 脚本中
_Gmoribund_unused // 5
// goroutine 没有被使用 (可能已经退出或刚刚初始化),没有在执行代码,可能存在分配的栈
_Gdead // 6
// 没有使用这个状态
_Genqueue_unused // 7
// goroutine 的栈正在被移动,没有在执行代码并且不在运行队列中
_Gcopystack // 8
// goroutine 由于抢占而阻塞,等待唤醒
_Gpreempted // 9
// GC 正在扫描栈空间,没有在执行代码,可以与上述其他状态同时存在
_Gscan = 0x1000
// 下面几个是组合状态
_Gscanrunnable = _Gscan + _Grunnable // 0x1001
_Gscanrunning = _Gscan + _Grunning // 0x1002
_Gscansyscall = _Gscan + _Gsyscall // 0x1003
_Gscanwaiting = _Gscan + _Gwaiting // 0x1004
_Gscanpreempted = _Gscan + _Gpreempted // 0x1009
)
调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(例如陷入系统调用或 IO 调用),最多只会有 GOMAXPROCS 个活跃线程能够正常运行。在默认情况下,运行时会将 GOMAXPROCS 设置成当前机器的核数,我们也可以在程序中使用 runtime.GOMAXPROCS 来改变最大的活跃线程数。
runtime.m0
表示 (全局只有一个实例)p
绑定,执行具体的 goroutine
逻辑代码
线程
的运行时表示。
type m struct {
g0 *g // 执行调度的 goroutine
...
curg *g // 当前运行的 goroutine
p puintptr // 正在运行代码的处理器 (如果为 nil, 说明当前没有代码运行)
nextp puintptr // 暂存的处理器
oldp puintptr // 执行系统调用之前使用线程的处理器
id int64 // ID
preemptoff string // 如果不为空,保持当前 goroutine 在这个 m 上运行
spinning bool // m 正在积极寻找活儿干
blocked bool // m 阻塞在 note
incgo bool // m 正在执行 cgo 调用
ncgocall uint64 // cgo 调用总次数
ncgo int32 // 当前正在运行的 cgo 调用次数
...
}
处理器是线程和 goroutine 的中间层,提供线程需要的上下文环境,负责调度线程上的等待队列,通过处理器 P 的调度, 每一个内核线程都能够执行多个 goroutine,它能在 goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的资源利用率。
处理器(p)
的运行时表示,线程(m)
必须持有 (绑定)p
才可以运行goroutine
。
type p struct {
id int32 // ID
status uint32 // p 的状态
schedtick uint32 // 调度时自增
syscalltick uint32 // 系统调用时自增
sysmontick sysmontick // sysmon 最后观察到的 tick 时间
m muintptr // 关联的 m 的指针,如果 p 处于空闲状态,指针为 nil
...
deferpool []*_defer // 可用的 _defer 对象池
deferpoolbuf [32]*_defer // _defer 对象池
goidcache uint64 // 缓存 goroutine ID, 优化 runtime·sched.goidgen
// goroutine 运行队列,访问时无需加锁
runqhead uint32 // runnable 队列头索引
runqtail uint32 // runnable 队列尾索引
runq [256]guintptr // runnable 队列 (环形队列,数据结构为数组,元素数量最多为 256)
// runnext 如果不等于 nil, 表示下一个可运行的 goroutine
// 说明它已经被当前 goroutine 修改为 ready 状态,并且比队列中的其他 goroutine 拥有更高的优先级
// 如果运行 goroutine 对应的时间片中还有剩余的时间,那么直接运行这个 goroutine,而不是放入队列中
runnext guintptr
...
}
const (
// 处理器没有在执行代码或者调度,处于空闲状态
_Pidle = iota
// 处理器被线程 M 持有,并且正在执行代码或者调度
_Prunning
// 处理器没有在执行代码,线程陷入系统调用
_Psyscall
// 处理器被线程 M 持有,由于 GC 被停止
_Pgcstop
// 处理器不再被使用
_Pdead
)
schedt
对象是全局调度器的运行时表示,全局只有一个 schedt
对象实例,定义在 $GOROOT/src/runtime/runtime2.go
文件中。
type schedt struct {
goidgen uint64 // 原子性访问,保持在 struct 顶部,确保 32 位系统上对齐
lastpoll uint64 // network poll 的最后时间,如果为 0, 说明正在 poll
pollUntil uint64 // 当前 poll 的休眠时间
lock mutex
// 增加 nmidle, nmidlelocked, nmsys, nmfreed 这几个值的时候, 确保调用 checkdead()
midle muintptr // 空闲的 m 队列
nmidle int32 // 空闲的 m 数量
nmidlelocked int32 // 空闲的被锁住的 m 数量
mnext int64 // 预创建的 m 数量,该数量会作为下一个创建的 m 的 ID
maxmcount int32 // 允许的 m 数量上限
nmsys int32 // 因为死锁未计算的系统 m 数量
nmfreed int64 // 累计释放的 m 数量
ngsys uint32 // 系统 goroutine 数量,原子性更新
pidle puintptr // 空闲的处理器队列
npidle uint32 // 空闲的处理器数量
runq gQueue // 全局可运行 goroutine 队列
runqsize int32 // 全局可运行 goroutine 数量
// dead 状态的 goroutine 的全局缓存
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
// sudog 对象的集中缓存
sudoglock mutex
sudogcache *sudog
// 可用的 _defer 对象的集中缓存
deferlock mutex
deferpool *_defer
// 当 m 被设置了 m.exited 标记之后,会挂载到 freem 链表上面等待被释放
// 链表使用 m.freelink 字段链接
freem *m
...
}
schedt
对象字段非常多 (毕竟是全局调度器),这里我们重点关注 3 个字段:
midle
表示空闲的 线程 (m)
,数据结构是指针,具体的 get + set
操作是通过 指针 + 位置偏移量
实现的pidle
表示空闲的 处理器 (p)
,数据结构和 midle
类似runq
表示可运行的 goroutine (g)
队列, 数据结构是链表本文主要对 GMP
调度器中的数据结构部分做了简单的概述:
g
对象表示 goroutine
, 是用来执行具体的任务的 (也就是干活的)m
对象表示 线程
, 和真正的 操作系统线程
绑定之后,就可以执行具体的 goroutine
代码了p
表示处理器,作为抽象中间层用来管理 goroutine
队列以及调度 goroutine
到具体的 m
上执行除此之外:
sudog
对象包装了一层 g
, 用来表示在队列中等待的 goroutine
对象gobuf
对象包装了一层 g
, 用来表示 goroutine
的运行现场,在调度器保存数据或者恢复上下文的时候可以用到最后,我们列出了 g
和 p
对象的不同状态值,这些值在程序整个生命周期内的调度过程中都会使用到。