Go GMP model
Machine
Go 对系统级线程的抽象,不直接执行 g,而是先和 p 绑定,由其实现代理
Processor
Go 中的调度器,是 m 的执行代理,p 的数量决定了 g 的最大并行数量(通过
runtime.GOMAXPROC(int)
设定)关联了本地可运行 G 的队列,队列最多可存放 256 个 G
Goroutine
参与调度与执行的最小单位
需要绑定到 p 才能执行,M 个 Goroutine 运行在 N 个内核线程之上,实现并行
创建、销毁、调度在用户态完成,切换成本比线程小
每个 Goroutine 有自己的运行栈,状态以及执行函数,能动态伸缩栈空间大小
// goroutine 状态
const (
_Gidle = iota // 0 协程开始创建的状态
_Grunnable // 1 协程在待执行队列
_Grunning // 2 协程正在执行,同一时刻一个 P 中只有一个 G 处于此状态
_Gsyscall // 3 协程正在执行系统调用
_Gwaiting // 4 协程处于挂起状态,等待被唤醒。GC、channel 通信或锁操作时经常会进入此状态
_Gdead // 6 协程刚初始化完成/已经被销毁
_Gcopystack // 8 协程正在栈扩容流程中
_Gpreempted // 9 协程被抢占后的状态
)
数据结构定义
GMP模型的数据结构定义在 runtime/runtime2.go
中。
type m struct {
g0 *g // 特殊的调度协程,负责执行 g 之间的切换调度
// thread-local storage, 线程本地存储,存储内容只对当前线程可见.
// 线程本地存储的是 m.tls 的地址,m.tls[0] 存储的是当前运行的 g,
// 因此线程可以通过 g 找到当前的 m、p、g0 等信息.
tls [tlsSlots]uintptr
// ...
}
type p struct {
runqhead uint32 // 队列头部
runqtail uint32 // 队列尾部
runq [256]guintptr // 本地 g 队列,最大长度为 256
runnext guintptr // 下一个可执行的 g
// ...
}
// schedt 全局 g 队列
type schedt struct {
lock mutex // 操作全局队列使用的锁
runq gQueue // 全局 g 队列
runqsize int32 // 全局 g 队列的容量
// ...
}
g0 和 g 的切换
m 通过 p 调度执行的 goroutine 永远在 g 和 g0 之间进行切换。当 g0 找到可执行的 g 时,会调用 gogo
方法,调度 g 执行用户定义的任务。当 g 需要主动让渡或被动调度时,会触发 mcall
方法,将执行权重新交还给 g0。
func gogo(buf *gobuf)
// ...
func mcall(fn func(*g))
宏观调度流程
g0 执行
schedule()
,寻找到用于执行的 gfindRunnable()
g0 执行
execute()
,更新当前 g、p 的状态信息,并调用gogo()
,将执行权交给 gg 因主动让渡(
gosche_m()
)、被动调度(park_m()
)、正常结束(goexit0()
)等原因,调用m_call()
,执行权重新回到 g0 手中g0 执行
schedule()
函数,开启新一轮循环
p 每 61 次调度,会去全局队列里取出一个 g(如果全局队列有g)并执行
// g0 为 m 寻找到可执行的 g 之后,接下来就开始执行 g
func execute(gp *g, inheritTime bool) {
_g_ := getg()
_g_.m.curg = gp
gp.m = _g_.m
casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
_g_.m.p.ptr().schedtick++
}
// 调用 gogo,执行 goroutine 中的任务
gogo(&gp.sched)
}
调度类型
这里是调度器 p 实现从执行一个 g 切换到另一个 g。
正常调度:g 执行任务完成,g0 将当前 g 置为 _Gdead
状态,发起新一轮调度。
抢占调度:在 Go 进程中有一个全局监控协程 monitor g(由于发起系统调用时需要打破用户态的边界进入内核态,此时 m 也会因系统调用而陷入僵直,无法主动完成抢占调度的行为),这个 g 会越过 p 直接与一个 m 进行绑定,不断轮询对所有 p 的执行状况进行监控,如果发现满足抢占调度条件(g 执行系统调用超过指定的时长,且 p 资源比较紧缺),则将 p 和 g 解绑,抢占出来用来于其他 g 的调度,等 g 完成系统调度后,会重新进入队列中等待被调度。
主动调度:用户主动让渡(即在代码中调用 runtime.Gosched()
),此时当前 g 会让出执行权,主动进入全局队列,等待下次被调度执行。
// g 执行主动让渡时,会调用 mcall 方法将执行权归还给 g0,并由 g0 调用 gosched_m 方法
func Gosched() {
// ...
mcall(gosched_m)
}
func gosched_m(gp *g) {
goschedImpl(gp)
}
func goschedImpl(gp *g) {
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
// 将当前 g 的状态由执行中切换为待执行 _Grunnable
casgstatus(gp, _Grunning, _Grunnable)
// dropg(),将当前的 m 和 g 解绑
dropg()
// 将 g 添加到全局队列当中
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
// 开启新一轮的调度
schedule()
被动调度:由于当前不满足某种执行条件,g 陷入阻塞态无法被调度,直到条件达成后,g 才从阻塞中被唤醒,重新进入可执行队列等待被调度(触发原因:channel 通信或互斥锁操作等,底层会进入
gopark
方法)。
// g 需要被动调度时,会调用 mcall 方法切换至 g0,并调用 park_m 方法将 g 置为阻塞态. goready 方法通常与 gopark 方法成对出现,能够将 g 从阻塞态中恢复,重新进入等待执行的状态.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
// ...
mcall(park_m)
}
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}