Go的GMP模型真的很"简单"
本文基于go1.19
前言
关于GMP模型网上已经有很多文章,讲的内容大多都是如下图的逻辑,本系列我们就不再赘述。本系列我们换个视角,核心是搞清楚两个问题:
- GMP到底是什么?
- goroutine如何恢复和保存上下文的?
正文开始。
GMP只是结构体
GMP并不是你想象的那么神奇的存在,其实就是普通的结构体,如同你写业务代码定义的结构体一样,如下:
// Goroutine |
// Machine |
// Processor |
GMP是系统线程运行的代码片段
GMP和你写的业务代码一样,都是由系统线程运行。
GMP是类似面相对象思想的封装
| 类型 | 结构体含义 | 结构体职责 |
|---|---|---|
G |
Goroutine,代表协程 | 1. 封装可被并发执行的函数片段,比如 go func() {// 函数A}() |
G |
- | 2. 暂存函数片段(协程)切换时的上下文信息 |
G |
- | 3. 封装g的栈内存空间,暂存函数片段(协程)执行时的临时变量的 |
M |
Machine,和系统线程建立映射,结构体绑定一个系统线程 | 1. 绑定真正执行代码的系统线程,系统线程执行G的调度,和被调度的G绑定的函数 |
M |
- | 2. 维护P链表(可以从下一个P的队列找G) |
P |
Processor,和逻辑处理器建立映射 | 1. 维护可执行G的队列(M从该队列找可执行的G); |
P |
- | 2. 堆内存缓存层(mcache) |
P |
- | 3. 维护g的闲置队列 |
G职责解析
接下来,展开关于G展开两个关键问题:
G和函数绑定过程G切换上下文过程
G和函数绑定过程
当你使用go关键字执行一个函数时go func(){}():
G和func具体绑定在哪?G和func何时绑定?
// `go`关键字示例 |
G和func具体绑定在哪?
位于g的结构体 g.startpc属性,详细如下:
// Goroutine |
G和func何时绑定?
- 当通过go关键字运行一个函数时
- 从g的闲置队列获取一个g,并通过
g.startpc属性绑定上待执行的函数fn
// 当你用go关键字执行一个函数 |
函数绑定过程如下:
G切换上下文过程
goroutine的上下文信息具体保存在哪?goroutine的上下文如何切换?
goroutine的上下文信息具体保存在哪?
位于g的结构体 g.sched属性,详细如下:
// Goroutine |
goroutine的上下文如何切换?
- g恢复上下文过程
- g保存上下文过程
g恢复上下文过程:
触发调度时:
- 找到可执行的g(来源本地队列、全局队列、netpoll list 读或写就绪的g列表)
- 把g的上下文
g.sched通过汇编代码中的函数gogo恢复到对应的寄存器中
// g的调度方法 |
gogo函数汇编代码,arm64架构示例汇编代码如下:
// void gogo(Gobuf*) |
g保存上下文过程:
其中两个关键函数如下
func save(pc, sp uintptr)触发保存上下文func mcall(fn func(*g))触发保存上下文
save函数
func save(pc, sp uintptr) { |
调用func save(pc, sp uintptr)的场景如下:
- 进入系统调用时
// 进入系统调用 |
mcall函数
func mcall(fn func(*g))执行过程中,从g切换到g0,并执行fn。fn内部会执行调度函数shedule(),触发新的调度,下面会举一个例子。
TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT|NOFRAME, $0-8 |
调用func mcall(fn func(*g))的场景如下:
Gosched():触发协作&抢占式式调度时gopark:g从运行状态转换为等待状态时goexit1()goroutine执行完成时exitsyscall()退出系统调用时- 等
详细展开,Gosched():触发协作&抢占式式调度时看看,如下
// 触发调度 |
- park_m 把g从运行状态转换为等待状态时
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { |
goexit1()goroutine执行完成时
func goexit1() { |
-
exitsyscall()退出系统调用时
func exitsyscall() { |
具体如下图:
总结下g的完整切换过程:
- 当前g保存上下文(save/mcall)
- 当前g切换到g0,g0执行
schedule调度,找到新的可执行的g - 新的g恢复上下文(gogo)
- 最后,实际以上操作都是有系统线程运行的
M职责解析
- 绑定真正执行代码的系统线程
- 系统线程执行
G的调度 - 系统线程执行被调度的
G绑定的函数 - 维护
P链表(可以从下一个P的队列找G)
// Machine |
P职责解析
- 维护可执行
G的队列(M从该队列找可执行的G); - 堆内存缓存层(
mcache) - 维护g的闲置队列
// Processor |
总结
再来回头看开篇的两个问题?
- GMP到底是什么?
- goroutine如何恢复和保存上下文的?
是不是已经很清晰。
- 关于问题一,GMP是三个各司其职的结构体,被系统线程运行。
| 类型 | 结构体含义 | 结构体职责 |
|---|---|---|
G |
Goroutine,代表协程 | 1. 封装可被并发执行的函数片段,比如 go func() {// 函数A}() |
G |
- | 2. 暂存函数片段(协程)切换时的上下文信息 |
G |
- | 3. 封装g的栈内存空间,暂存函数片段(协程)执行时的临时变量的 |
M |
Machine,和系统线程建立映射,结构体绑定一个系统线程 | 1. 绑定真正执行代码的系统线程,系统线程执行G的调度,和被调度的G绑定的函数 |
M |
- | 2. 维护P链表(可以从下一个P的队列找G) |
P |
Processor,和逻辑处理器建立映射 | 1. 维护可执行G的队列(M从该队列找可执行的G); |
P |
- | 2. 堆内存缓存层(mcache) |
P |
- | 3. 维护g的闲置队列 |
关于问题二,goroutine恢复和保存上下文过程:
当前g保存上下文(save/mcall)
当前g切换到g0,g0执行
schedule调度,找到新的可执行的g新的g恢复上下文(gogo)
具体如下图所示:
