Go Goroutine与GMP模型
Go调度器通过GMP模型实现轻量级并发,用户级调度高效切换。
GMP模型概述
三大组件
Go
G(Goroutine):用户协程
M(Machine):操作系统线程
P(Processor):调度处理器(逻辑核心)
关系示意
Go
G队列
↓
P(调度器)← 本地队列
↓
M(执行线程)
↓
CPU核心
P的数量默认等于CPU核心数(GOMAXPROCS)。
G(Goroutine)
结构定义
Go
type g struct {
stack stack // 栈空间(2KB起步)
sched gobuf // 调度上下文(PC、SP)
status uint32 // 状态
goid int64 // Goroutine ID
m *m // 绑定的M
atomicstatus uint32 // 原子状态
}
type gobuf struct {
sp uintptr // 栈指针
pc uintptr // 程序计数器
g uintptr // Goroutine指针
}
G状态表
| 状态 | 含义 | 说明 |
|---|---|---|
| _Gidle | 初始状态 | 刚分配 |
| _Grunnable | 可运行 | 在队列等待 |
| _Grunning | 正在运行 | 被M执行 |
| _Gwaiting | 等待状态 | 阻塞中 |
| _Gsyscall | 系统调用 | 执行syscall |
| _Gdead | 结束状态 | 已退出 |
M(Machine)
结构定义
Go
type m struct {
g0 *g // 特殊G(调度栈)
curg *g // 当前执行的G
p *p // 绑定的P
nextp *p // 下一个P
spinning bool // 是否自旋寻找G
park note // 休眠通知
parknote *note // 休眠条件
}
M状态
Go
运行中:绑定P执行G
自旋:寻找可执行G(无G但有P)
休眠:无G无P,等待唤醒
g0是M的调度栈,用于执行调度代码。
P(Processor)
结构定义
Go
type p struct {
id int32 // P的ID
status uint32 // 状态
m *m // 绑定的M
// 本地G队列
runq [256]*g // 本地队列
runqhead uint32 // 队头
runqtail uint32 // 队尾
runnext *g // 优先执行的G
gFree *g // G缓存池
}
P状态表
| 状态 | 含义 | 说明 |
|---|---|---|
| _Pidle | 空闲 | 无G可执行 |
| _Prunning | 运行 | 绑定M执行G |
| _Psyscall | 系统调用 | M在syscall中 |
| _Pgcstop | GC停止 | GC时暂停 |
| _Pdead | 结束 | 不再使用 |
调度流程
schedule函数
Go
func schedule() {
// 1. 检查runnext(优先)
if gp := runnext; gp != nil {
execute(gp)
return
}
// 2. 从本地队列获取
gp := runqget()
if gp != nil {
execute(gp)
return
}
// 3. 从全局队列获取
gp := globrunqget()
if gp != nil {
execute(gp)
return
}
// 4. 工作窃取
gp := stealWork()
if gp != nil {
execute(gp)
return
}
// 5. 没有G则休眠
park()
}
调度优先级
Go
runnext(刚唤醒的G)
↓ 最高优先
本地队列(runq)
↓
全局队列(globrunq)
↓
工作窃取(其他P的队列)
↓
netpoller(网络就绪G)
↓
休眠等待
工作窃取
算法原理
Go
func stealWork() *g {
// 随机选择其他P
p2 := allp[fastrand()%n]
// 窃取一半G
n := runqsteal(p2, p)
if n > 0 {
return stolenG
}
return nil
}
窃取示意
Go
P1队列:G1 G2 G3 G4
↓ 窃取一半
P2队列:G1 G2(偷来)
工作窃取实现负载均衡,避免部分P过忙。
系统调用处理
entersyscall
Go
func entersyscall() {
// P状态改为_Psyscall
P.status = _Psyscall
// M解绑P(允许其他M接管)
// 如果syscall超过10ms
// sysmon会handoffp
}
exitsyscall
Bash
func exitsyscall() {
// 尝试重新获取原P
if tryReacquireP() {
// 成功继续执行
P.status = _Prunning
return
}
// 失败:G放入全局队列
// M进入休眠
globrunqput(g)
parkm()
}
长时间syscall会释放P,避免阻塞其他G。
抢占调度
基于信号抢占
Go
// Go 1.14+基于信号的异步抢占
func preemptone(p) {
// 发送SIGURG信号
signalM(m, sigPreempt)
// M收到信号后
// 在安全点暂停G
// 调度执行其他G
}
抢占触发条件
text
G运行超过10ms
↓ sysmon检测
发送抢占信号
↓
G在安全点暂停
↓
重新调度
防止一个G长时间占用CPU,实现公平调度。
Goroutine创建
newproc流程
text
func newproc(fn) {
// 1. 获取或创建G
newg := gfget()
if newg == nil {
newg = malg() // 新建G
}
// 2. 初始化G
newg.status = _Grunnable
newg.sched.pc = fn
// 3. 放入队列
runqput(p, newg)
// 4. 尝试唤醒M
if atomicload(&m.spinning) == 0 {
wakep()
}
}
G栈管理
text
初始栈:2KB
↓ 动态增长
最大栈:1GB(64位)
↓ 栈收缩
释放后:复用或回收
Goroutine栈动态伸缩,相比线程栈节省内存。
调度器配置
GOMAXPROCS
text
import "runtime"
// 设置P数量(CPU核心数)
runtime.GOMAXPROCS(4)
// 获取当前设置
n := runtime.GOMAXPROCS(0)
NumGoroutine
text
// 获取当前Goroutine数量
n := runtime.NumGoroutine()
fmt.Println(n) // 活跃G数量
调度信息查看
GODEBUG调试
text
# 输出调度信息
GODEBUG=schedtrace=1000 ./app
# 输出示例
SCHED 1000ms: gomaxprocs=8 idleprocs=6 threads=10 ...
schedtrace字段
text
gomaxprocs:P数量
idleprocs:空闲P数量
threads:M总数
idlethreads:空闲M数量
runnablequeue:全局队列G数
runnable:总可运行G数
handoff机制
sysmon监控
text
func sysmon() {
// 每10ms检查一次
// 1. 检查抢占
retakeP()
// 2. 检查系统调用
if p.status == _Psyscall && elapsed > 10ms {
handoffp(p) // P交给其他M
}
// 3. 检查netpoller
netpoll()
}
调度器核心机制表
| 机制 | 作用 | 场景 |
|---|---|---|
| 工作窃取 | 负载均衡 | P空闲时 |
| 系统调用handoff | P释放 | syscall阻塞 |
| 基于信号抢占 | 防止饿死 | G运行过长 |
| netpoller | 网络I/O | 非阻塞I/O |
| runnext | 优先调度 | 刚唤醒G |
要点总结
- G是Goroutine,M是线程,P是调度处理器
- P数量默认等于CPU核心数
- 本地队列实现无锁调度
- 工作窃取实现负载均衡
- entersyscall/exitsyscall处理系统调用
- sysmon监控抢占和handoff
- 基于信号实现异步抢占
- runnext优先调度刚唤醒的G
- GOMAXPROCS设置P数量
- NumGoroutine获取活跃G数
- G栈动态伸缩,初始2KB
📝 发现内容有误?点击此处直接编辑