GPM设计原理解析
基于Go 1.23.3源码深度解析GMP调度器的设计原理、实现机制和性能优化策略
1. GMP模型概述
1.1 设计理念
Go语言的GMP调度器是一个用户级线程调度器,采用M:N的调度模型,其中:
- G (Goroutine): 用户级轻量线程,Go协程
- M (Machine): 内核线程,操作系统线程的抽象
- P (Processor): 逻辑处理器,连接M和G的桥梁
1.2 架构设计图
GMP调度器整体架构:
graph TB
subgraph "Global Scheduler"
GRQ["🌐 Global Run Queue<br/>[G10][G11][G12]..."]
SYSMON["🔍 sysmon goroutine<br/>(System Monitor)"]
end
subgraph "P0 (Processor 0)"
P0["📊 Processor P0"]
LRQ0["📋 Local RunQ<br/>[G1][G2][G3]"]
P0 --- LRQ0
end
subgraph "P1 (Processor 1)"
P1["📊 Processor P1"]
LRQ1["📋 Local RunQ<br/>[G4][G5][G6]"]
P1 --- LRQ1
end
subgraph "P2 (Processor 2)"
P2["📊 Processor P2"]
LRQ2["📋 Local RunQ<br/>[G7][G8][G9]"]
P2 --- LRQ2
end
M0["🧵 M0<br/>(OS Thread)"]
M1["🧵 M1<br/>(OS Thread)"]
M2["🧵 M2<br/>(OS Thread)"]
M0 -.-> P0
M1 -.-> P1
M2 -.-> P2
GRQ -.-> P0
GRQ -.-> P1
GRQ -.-> P2
classDef processor fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef machine fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef queue fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef global fill:#fff3e0,stroke:#e65100,stroke-width:2px
class P0,P1,P2 processor
class M0,M1,M2 machine
class LRQ0,LRQ1,LRQ2 queue
class GRQ,SYSMON global
架构说明:
- 🧵 M (Machine): 操作系统线程,负责执行goroutine
- 📊 P (Processor): 逻辑处理器,维护本地运行队列
- 📋 Local RunQ: 本地运行队列,存储待执行的goroutine
- 🌐 Global RunQ: 全局运行队列,负载均衡和公平调度
- 🔍 sysmon: 系统监控器,负责抢占、GC触发等
1.3 核心优势
- 高并发性能: 支持百万级goroutine
- 低调度开销: 用户级调度,避免内核态切换
- 智能负载均衡: 工作窃取机制
- 抢占式调度: 防止goroutine长时间占用CPU
2. 核心数据结构源码分析
Go语言的GMP调度器基于四个核心数据结构:g
、m
、p
和schedt
。每个结构都有特定的作用和设计目标。我们将逐一深入分析它们的源码实现。
2.1 调度上下文结构 (gobuf)
核心作用: gobuf
是goroutine调度切换时保存和恢复CPU寄存器状态的关键结构,实现了用户级线程的上下文切换机制。
首先分析goroutine上下文切换的核心结构gobuf
:
// 源码位置:go/src/runtime/runtime2.go
type gobuf struct {
// sp, pc, g的偏移量是已知的(硬编码在)libmach中
//
// ctxt在GC方面是不寻常的:它可能是堆分配的funcval,
// 所以GC需要跟踪它,但它需要从汇编中设置和清除,
// 在那里很难有写屏障。然而,ctxt实际上是一个保存的活动寄存器,
// 我们只在真实寄存器和gobuf之间交换它。
// 因此,我们在栈扫描期间将其视为根,这意味着保存和恢复它的汇编不需要写屏障
// 它仍然被类型化为指针,以便Go的任何其他写入都获得写屏障
sp uintptr // 栈指针
pc uintptr // 程序计数器
g guintptr // goroutine指针(绕过写屏障)
ctxt unsafe.Pointer // 上下文指针,通常指向funcval
ret uintptr // 返回值
lr uintptr // 链接寄存器(用于某些架构)
bp uintptr // 基指针(用于启用帧指针的架构)
}
gobuf的核心作用:
- 上下文保存: 在goroutine调度切换时保存CPU寄存器状态
- 恢复执行: 在goroutine重新调度时恢复之前的执行状态
- 跨平台兼容: 支持不同CPU架构的寄存器模型
2.2 Goroutine (G) 结构
核心作用: g
代表一个goroutine实例,包含了goroutine的完整执行状态、栈信息、调度上下文和GC相关数据,是Go并发模型的基本执行单元。
// 源码位置:go/src/runtime/runtime2.go
type g struct {
// 栈参数
// stack描述实际的栈内存:[stack.lo, stack.hi)
// stackguard0是在Go栈增长序言中比较的栈指针
// 通常是stack.lo+StackGuard,但可以是StackPreempt来触发抢占
stack stack // 栈信息,偏移量对runtime/cgo已知
stackguard0 uintptr // 栈保护字0,偏移量对liblink已知
stackguard1 uintptr // 栈保护字1,偏移量对liblink已知
_panic *_panic // 最内层的panic,偏移量对liblink已知
_defer *_defer // 最内层的defer
m *m // 当前的m,偏移量对arm liblink已知
sched gobuf // 调度上下文
syscallsp uintptr // 如果status==Gsyscall,syscallsp = sched.sp
syscallpc uintptr // 如果status==Gsyscall,syscallpc = sched.pc
// 调度相关核心字段
atomicstatus atomic.Uint32 // goroutine状态,原子访问
goid uint64 // goroutine ID
schedlink guintptr // 调度链接,用于链接goroutine队列
waitsince int64 // g变为阻塞的大致时间
waitreason waitReason // 如果status==Gwaiting,等待的原因
// 抢占控制
preempt bool // 抢占信号,复制stackguard0 = stackpreempt
preemptStop bool // 抢占时转换到_Gpreempted;否则,只是取消调度
preemptShrink bool // 在同步安全点收缩栈
// 其他重要字段...
lockedm muintptr // 锁定的m
gopc uintptr // 创建此goroutine的go语句的pc
startpc uintptr // goroutine函数的pc
}
G的状态转换图:
stateDiagram-v2
[*] --> Gidle : newproc分配
Gidle --> Grunnable : 初始化完成
Grunnable --> Grunning : 调度器execute
Grunning --> Gwaiting : gopark阻塞
Grunning --> Grunnable : 时间片用完/抢占
Grunning --> Gsyscall : 系统调用
Grunning --> Gdead : goexit结束
Gwaiting --> Grunnable : goready唤醒
Gsyscall --> Grunnable : 系统调用返回
Gdead --> [*] : GC回收
note right of Gidle
已分配内存
未设置栈
end note
note right of Grunnable
在运行队列中
等待被调度
end note
note right of Grunning
正在CPU上执行
占用M和P资源
end note
note right of Gwaiting
等待资源
让出CPU
end note
note right of Gsyscall
执行系统调用
可能释放P
end note
状态常量定义:
// 源码位置:go/src/runtime/runtime2.go
const (
_Gidle = iota // 0: goroutine刚分配,尚未初始化
_Grunnable // 1: 在运行队列中,未执行用户代码
_Grunning // 2: 不在运行队列中,可能执行用户代码
_Gsyscall // 3: 不在运行队列中,执行系统调用
_Gwaiting // 4: 运行时被阻塞,不执行用户代码
_Gdead // 6: 当前未使用,可能刚退出或在释放列表中
// ...其他状态
)
2.3 Machine (M) 结构
核心作用: m
代表一个OS线程(机器),是goroutine的实际执行载体,负责执行goroutine并管理与操作系统的交互。
// 源码位置:go/src/runtime/runtime2.go
type m struct {
g0 *g // 带有调度栈的goroutine(系统栈)
morebuf gobuf // morestack的gobuf参数
divmod uint32 // arm的div/mod分母 - liblink已知
// 调试器不知道的字段
procid uint64 // 进程ID,用于调试器
gsignal *g // 信号处理g
goSigStack gsignalStack // Go分配的信号处理栈
sigmask sigset // 保存的信号掩码存储
tls [tlsSlots]uintptr // 线程本地存储
mstartfn func() // M启动函数
curg *g // 当前运行的goroutine
caughtsig guintptr // 在致命信号期间运行的goroutine
p puintptr // 执行go代码的关联p
nextp puintptr // 下一个P
oldp puintptr // 执行系统调用前关联的p
id int64 // M的唯一ID
// 调度状态
spinning bool // m无工作并且正在积极寻找工作
blocked bool // m在note上阻塞
lockedg guintptr // 锁定的goroutine
// 其他重要字段...
}
关键方法 - Spinning状态:
// 源码位置:go/src/runtime/proc.go
func (mp *m) becomeSpinning() {
mp.spinning = true
sched.nmspinning.Add(1)
}
2.4 Processor (P) 结构
核心作用: p
代表逻辑处理器,是M和G之间的桥梁,管理本地运行队列、内存分配缓存和定时器,实现了工作窃取算法的负载均衡。
// 源码位置:go/src/runtime/runtime2.go
type p struct {
id int32 // P的唯一ID
status uint32 // P的状态:pidle/prunning等之一
link puintptr // 链接到下一个P(用于空闲P列表)
schedtick uint32 // 每次调度器调用时递增
syscalltick uint32 // 每次系统调用时递增
sysmontick sysmontick // sysmon观察到的最后一次tick
m muintptr // 关联的m的反向链接
mcache *mcache // 内存分配缓存
// 本地运行队列
runqhead uint32 // 运行队列头部索引
runqtail uint32 // 运行队列尾部索引
runq [256]guintptr // 运行队列数组
// runnext如果非nil,是由当前G准备好的可运行G,
// 如果在当前G的时间片中有剩余时间,应该下一个运行而不是runq中的
runnext guintptr
// 可用的G(status == Gdead)
gFree struct {
gList // G的列表
n int32 // 列表中G的数量
}
// 其他重要字段...
}
P的状态转换:
// 源码位置:go/src/runtime/runtime2.go
const (
_Pidle = iota // 0: P未与M关联,空闲状态
_Prunning // 1: P与M关联,执行用户代码
_Psyscall // 2: P与M关联,执行系统调用
_Pgcstop // 3: P被halted,参与STW
_Pdead // 4: P不再使用(GOMAXPROCS缩减)
)
2.5 全局调度器 (schedt) 结构
核心作用: schedt
是全局调度器的中央控制结构,管理所有M和P的生命周期、全局运行队列、GC协调和系统监控,是整个GMP调度系统的核心枢纽。
最重要的全局调度器数据结构:
// 源码位置:go/src/runtime/runtime2.go
type schedt struct {
goidgen atomic.Uint64 // goroutine ID生成器
lastpoll atomic.Int64 // 上次网络轮询时间,如果当前正在轮询则为0
pollUntil atomic.Int64 // 当前轮询睡眠到的时间
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 int64 // 不计入死锁的系统m数量
nmfreed int64 // 累计释放的m数量
ngsys atomic.Int32 // 系统goroutine数量
pidle puintptr // 空闲p
npidle atomic.Int32 // 空闲P数量
nmspinning atomic.Int32 // 参见proc.go中的"工作线程停放/取消停放"注释
needspinning atomic.Uint32 // 参见proc.go中的"精妙舞蹈"注释
// 全局可运行队列
runq gQueue // 全局运行队列
runqsize int32 // 全局运行队列大小
// disable控制调度器的选择性禁用
disable struct {
user bool // user禁用用户goroutine的调度
runnable gQueue // 待处理的可运行G
n int32 // runnable的长度
}
// 死G的全局缓存
gFree struct {
lock mutex // gFree的锁
stack gList // 有栈的G
noStack gList // 无栈的G
n int32 // 总数
}
// 同步原语缓存
sudoglock mutex // sudog锁
sudogcache *sudog // sudog缓存
deferlock mutex // defer锁
deferpool *_defer // defer池
// 其他重要字段...
gcwaiting atomic.Bool // gc正在等待运行
sysmonwait atomic.Bool // sysmon等待标志
// 性能统计
timeToRun timeHistogram // 调度延迟分布
idleTime atomic.Int64 // P空闲时间
}
schedt全局实例:
// 源码位置:go/src/runtime/proc.go sched schedt
3. 调度策略深入解析
3.1 调度策略概述
Go语言的GMP调度器采用了一套精密的调度策略,其核心目标是:
- 高效率: 最大化CPU利用率,减少调度开销
- 公平性: 防止goroutine饥饿,保证调度公平
- 负载均衡: 通过工作窃取实现P之间的负载均衡
- 响应性: 支持抢占式调度,保证系统响应
我们通过go
关键字来创建并执行一个goroutine,底层会调用newproc
函数,完成goroutine的创建和初始调度。
3.2 Goroutine创建机制
创建入口函数:
// 源码位置:go/src/runtime/proc.go
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc, false, waitReasonZero)
pp := getg().m.p.ptr()
runqput(pp, newg, true) // 放入P的本地协程队列
if mainStarted {
wakep() // 尝试唤醒空闲P
}
})
}
3.3 调度核心策略 - schedule函数解析
调度器的核心逻辑在schedule()
函数中,它按照以下精心设计的步骤来寻找下一个可执行的goroutine:
// 源码位置:go/src/runtime/proc.go
func schedule() {
mp := getg().m
pp := mp.p.ptr()
top:
var gp *g
var inheritTime bool
// 步骤1: 每61次调度检查一次全局队列
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false
}
}
// 步骤2: 检查本地运行队列
if gp == nil {
gp, inheritTime = runqget(pp)
}
// 步骤3: 再次检查全局运行队列
if gp == nil {
gp, inheritTime, _, _, _ = findrunnable()
}
// 执行找到的goroutine
execute(gp, inheritTime)
}
步骤1: 每61次调度检查一次全局队列
设计目的: 防止全局队列中的goroutine饥饿,保证调度公平性。
状态转换图 - 检查前:
graph TB
subgraph "当前状态 - 准备检查全局队列"
subgraph "Global Scheduler"
GRQ["🌐 全局运行队列<br/>[G10][G11][G12]<br/>等待被调度"]
COUNTER["📊 schedtick % 61 == 0<br/>触发全局队列检查"]
end
subgraph "P0处理器"
P0["📊 P0 - 当前活跃"]
LRQ0["📋 本地队列<br/>[G1][G2][G3]"]
P0 --- LRQ0
end
M0["🧵 M0<br/>正在执行schedule()"]
M0 -.-> P0
COUNTER -.-> GRQ
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef waiting fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef checking fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class M0,P0 active
class GRQ waiting
class COUNTER checking
核心源码分析:
// 源码位置:go/src/runtime/proc.go
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(pp, 1) // 从全局队列获取1个goroutine
unlock(&sched.lock)
if gp != nil {
return gp, false
}
}
状态转换图 - 检查后:
graph TB
subgraph "状态转换 - 全局队列检查完成"
subgraph "Global Scheduler"
GRQ2["🌐 全局运行队列<br/>[G11][G12]<br/>G10被取出"]
RESULT["✅ 获取到G10<br/>准备执行"]
end
subgraph "P0处理器"
P0_2["📊 P0 - 获得G10"]
LRQ0_2["📋 本地队列<br/>[G1][G2][G3]<br/>保持不变"]
CURRENT["🎯 当前要执行: G10"]
P0_2 --- LRQ0_2
P0_2 --- CURRENT
end
M0_2["🧵 M0<br/>准备执行G10"]
M0_2 -.-> P0_2
GRQ2 --> RESULT
RESULT --> CURRENT
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef success fill:#c8e6c9,stroke:#388e3c,stroke-width:3px
classDef selected fill:#ffcdd2,stroke:#d32f2f,stroke-width:3px
class M0_2,P0_2 active
class RESULT success
class CURRENT selected
步骤2: 检查本地运行队列
设计目的: 优先执行本地队列中的goroutine,减少锁竞争,提高缓存局部性。
状态转换图 - 检查前:
graph TB
subgraph "当前状态 - 检查本地队列"
subgraph "P0处理器"
P0["📊 P0 - 活跃状态"]
LRQ0["📋 本地运行队列<br/>[G1][G2][G3]<br/>头部: G1 准备执行"]
RUNNEXT["🎯 runnext: G4<br/>优先级最高"]
P0 --- LRQ0
P0 --- RUNNEXT
end
M0["🧵 M0<br/>执行runqget()"]
M0 -.-> P0
CHECK["🔍 检查顺序:<br/>1. runnext<br/>2. 本地队列头部"]
CHECK -.-> RUNNEXT
CHECK -.-> LRQ0
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef priority fill:#f8bbd9,stroke:#e91e63,stroke-width:3px
classDef queue fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef process fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class M0,P0 active
class RUNNEXT priority
class LRQ0 queue
class CHECK process
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func runqget(pp *p) (gp *g, inheritTime bool) {
// 1. 优先检查runnext
next := pp.runnext
if next != 0 && pp.runnext.cas(next, 0) {
return next.ptr(), true
}
// 2. 从本地队列头部获取
for {
h := atomic.LoadAcq(&pp.runqhead)
t := pp.runqtail
if t == h {
return nil, false // 队列为空
}
gp := pp.runq[h%uint32(len(pp.runq))].ptr()
if atomic.CasRel(&pp.runqhead, h, h+1) {
return gp, false
}
}
}
状态转换图 - 检查后:
graph TB
subgraph "状态转换 - 本地队列检查完成"
subgraph "P0处理器"
P0_2["📊 P0 - 获得G4"]
LRQ0_2["📋 本地运行队列<br/>[G1][G2][G3]<br/>保持不变"]
RUNNEXT_2["🎯 runnext: nil<br/>G4被取出"]
SELECTED["✅ 选中G4执行<br/>inheritTime=true"]
P0_2 --- LRQ0_2
P0_2 --- RUNNEXT_2
P0_2 --- SELECTED
end
M0_2["🧵 M0<br/>准备执行G4"]
M0_2 -.-> P0_2
REASON["💡 选择G4原因:<br/>runnext优先级最高<br/>继承时间片"]
REASON -.-> SELECTED
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef success fill:#c8e6c9,stroke:#388e3c,stroke-width:3px
classDef empty fill:#f5f5f5,stroke:#9e9e9e,stroke-width:2px
classDef info fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
class M0_2,P0_2 active
class SELECTED success
class RUNNEXT_2 empty
class REASON info
步骤3: 再次检查全局运行队列
设计目的: 当本地队列为空时,从全局队列批量获取多个goroutine,提高效率。
状态转换图 - 批量获取前:
graph TB
subgraph "当前状态 - 本地队列已空,检查全局队列"
subgraph "Global Scheduler"
GRQ["🌐 全局运行队列<br/>[G10][G11][G12][G13][G14]<br/>5个等待中的goroutine"]
BATCH["📦 批量获取策略<br/>最多获取 len(runq)/2"]
end
subgraph "P0处理器"
P0["📊 P0 - 本地队列空"]
LRQ0["📋 本地队列: []<br/>完全为空"]
P0 --- LRQ0
end
M0["🧵 M0<br/>执行globrunqget()"]
M0 -.-> P0
GRQ --> BATCH
end
classDef empty fill:#ffebee,stroke:#c62828,stroke-width:3px
classDef waiting fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef strategy fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class P0,LRQ0 empty
class GRQ waiting
class BATCH strategy
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func globrunqget(pp *p, max int32) *g {
sched.lock.Lock()
defer sched.lock.Unlock()
n := sched.runqsize/gomaxprocs + 1 // 计算本次获取数量
if n > sched.runqsize {
n = sched.runqsize
}
if max > 0 && n > max {
n = max
}
if n > int32(len(pp.runq))/2 {
n = int32(len(pp.runq)) / 2 // 最多装满本地队列的一半
}
sched.runqsize -= n
gp := sched.runq.pop() // 取第一个作为返回值
n--
// 剩余的放入本地队列
for ; n > 0; n-- {
gp1 := sched.runq.pop()
runqput(pp, gp1, false)
}
return gp
}
状态转换图 - 批量获取后:
graph TB
subgraph "状态转换 - 全局队列批量获取完成"
subgraph "Global Scheduler"
GRQ2["🌐 全局运行队列<br/>[G13][G14]<br/>剩余2个goroutine"]
RESULT2["✅ 批量获取3个<br/>G10,G11,G12"]
end
subgraph "P0处理器"
P0_2["📊 P0 - 获得工作"]
LRQ0_2["📋 本地队列<br/>[G11][G12]<br/>2个进入队列"]
CURRENT2["🎯 当前执行: G10<br/>第一个直接返回"]
P0_2 --- LRQ0_2
P0_2 --- CURRENT2
end
M0_2["🧵 M0<br/>准备执行G10"]
M0_2 -.-> P0_2
EFFICIENCY["⚡ 效率优化:<br/>一次获取多个goroutine<br/>减少全局锁竞争"]
EFFICIENCY -.-> RESULT2
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef filled fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef selected fill:#ffcdd2,stroke:#d32f2f,stroke-width:3px
classDef info fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
class M0_2,P0_2 active
class LRQ0_2 filled
class CURRENT2 selected
class EFFICIENCY info
步骤4: 轮询网络
设计目的: 检查是否有网络I/O事件就绪,唤醒等待的goroutine,提高系统响应性。
状态转换图 - 网络轮询前:
graph TB
subgraph "当前状态 - 所有队列为空,进行网络轮询"
subgraph "Network Poller"
NETPOLL["🌐 网络轮询器<br/>epoll/kqueue/IOCP"]
WAITING["⏳ 等待中的网络事件<br/>TCP连接、文件I/O等"]
NETPOLL --- WAITING
end
subgraph "P0处理器"
P0["📊 P0 - 无工作可做"]
LRQ0["📋 本地队列: 空"]
P0 --- LRQ0
end
subgraph "Blocked Goroutines"
BG1["😴 G15: 等待网络读取"]
BG2["😴 G16: 等待网络写入"]
BG3["😴 G17: 等待连接建立"]
end
M0["🧵 M0<br/>调用netpoll()"]
M0 -.-> P0
POLL_ACTION["🔍 轮询检查:<br/>检查就绪的网络事件<br/>返回可运行的goroutine"]
POLL_ACTION -.-> NETPOLL
POLL_ACTION -.-> BG1
POLL_ACTION -.-> BG2
end
classDef empty fill:#ffebee,stroke:#c62828,stroke-width:3px
classDef network fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
classDef blocked fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
classDef action fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class P0,LRQ0 empty
class NETPOLL,WAITING network
class BG1,BG2,BG3 blocked
class POLL_ACTION action
核心源码分析:
// 源码位置:go/src/runtime/proc.go
// 在findrunnable函数中的网络轮询逻辑
if netpollinited() && netpollAnyWaiters() && sched.lastpoll.Load() != 0 {
if list, delta := netpoll(0); !list.empty() { // 非阻塞调用
gp := list.pop() // 取出一个ready的goroutine直接返回
injectglist(&list) // 将其余的注入全局队列
netpollAdjustWaiters(delta) // 调整等待者计数
trace := traceAcquire()
casgstatus(gp, _Gwaiting, _Grunnable) // 改变goroutine状态
if trace.ok() {
trace.GoUnpark(gp, 0)
traceRelease(trace)
}
return gp, false, false // 找到网络ready的goroutine
}
}
// 源码位置:go/src/runtime/proc.go
func injectglist(glist *gList) {
if glist.empty() {
return
}
lock(&sched.lock)
var n int
for n = 0; !glist.empty(); n++ {
gp := glist.pop()
casgstatus(gp, _Gwaiting, _Grunnable)
globrunqput(gp) // 其余goroutine放入全局队列
}
unlock(&sched.lock)
// 尝试启动新的M来处理这些goroutine
for ; n != 0 && sched.npidle.Load() != 0; n-- {
startm(nil, false, false)
}
}
状态转换图 - 网络轮询后:
graph TB
subgraph "状态转换 - 网络轮询完成,goroutine分配"
subgraph "Network Poller"
NETPOLL2["🌐 网络轮询器<br/>发现3个就绪事件"]
READY["✅ 就绪事件:<br/>G15: 数据可读<br/>G16: 连接可写<br/>G17: 文件I/O完成"]
NETPOLL2 --- READY
end
subgraph "Global Scheduler"
GRQ_NEW["🌐 全局运行队列<br/>[G16][G17]<br/>其余网络goroutine注入"]
end
subgraph "P0处理器"
P0_2["📊 P0 - 获得G15"]
LRQ0_2["📋 本地队列: 空<br/>保持不变"]
CURRENT3["🎯 当前执行: G15<br/>第一个网络就绪G<br/>直接返回执行"]
P0_2 --- LRQ0_2
P0_2 --- CURRENT3
end
M0_2["🧵 M0<br/>执行网络就绪的G15"]
M0_2 -.-> P0_2
LOGIC["📝 分配逻辑:<br/>list.pop() → 直接返回<br/>injectglist() → 其余进全局队列"]
WAKEUP["🚀 可能唤醒更多M<br/>处理全局队列中的G16,G17"]
READY --> LOGIC
LOGIC --> CURRENT3
LOGIC --> GRQ_NEW
GRQ_NEW -.-> WAKEUP
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef ready fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef network fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
classDef selected fill:#ffcdd2,stroke:#d32f2f,stroke-width:3px
classDef logic fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef action fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class M0_2,P0_2 active
class READY ready
class NETPOLL2 network
class CURRENT3 selected
class LOGIC logic
class WAKEUP action
步骤5: 工作窃取
设计目的: 从其他P的本地队列窃取goroutine,实现负载均衡。
状态转换图 - 窃取前:
graph TB
subgraph "当前状态 - 准备工作窃取"
subgraph "P0处理器(空闲)"
P0["📊 P0 - 无工作"]
LRQ0["📋 本地队列: 空"]
P0 --- LRQ0
end
subgraph "P1处理器(繁忙)"
P1["📊 P1 - 工作饱和"]
LRQ1["📋 本地队列<br/>[G5][G6][G7][G8]<br/>4个goroutine"]
P1 --- LRQ1
end
subgraph "P2处理器(繁忙)"
P2["📊 P2 - 工作饱和"]
LRQ2["📋 本地队列<br/>[G9][G10][G11]<br/>3个goroutine"]
P2 --- LRQ2
end
M0["🧵 M0<br/>寻找工作"]
M0 -.-> P0
STEAL["🔍 工作窃取策略:<br/>随机选择目标P<br/>窃取一半goroutine"]
STEAL -.-> P1
STEAL -.-> P2
end
classDef empty fill:#ffebee,stroke:#c62828,stroke-width:3px
classDef busy fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef process fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class P0,LRQ0 empty
class P1,P2,LRQ1,LRQ2 busy
class STEAL process
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func runqsteal(pp, p2 *p, stealRunNextG bool) *g {
t := p2.runqtail
n := t - p2.runqhead
if n == 0 {
return nil // 目标队列为空
}
n = n / 2 // 窃取一半
if n == 0 {
return nil
}
// 从队列尾部窃取
return p2.runq[(t-1)%uint32(len(p2.runq))].ptr()
}
状态转换图 - 窃取后:
graph TB
subgraph "状态转换 - 工作窃取完成"
subgraph "P0处理器(获得工作)"
P0_2["📊 P0 - 获得G6,G7"]
LRQ0_2["📋 本地队列<br/>[G6][G7]<br/>窃取成功"]
CURRENT_2["🎯 当前执行: G6"]
P0_2 --- LRQ0_2
P0_2 --- CURRENT_2
end
subgraph "P1处理器(被窃取)"
P1_2["📊 P1 - 失去一半工作"]
LRQ1_2["📋 本地队列<br/>[G5][G8]<br/>剩余2个"]
P1_2 --- LRQ1_2
end
subgraph "P2处理器(未受影响)"
P2_2["📊 P2 - 保持现状"]
LRQ2_2["📋 本地队列<br/>[G9][G10][G11]"]
P2_2 --- LRQ2_2
end
M0_2["🧵 M0<br/>执行G6"]
M0_2 -.-> P0_2
BALANCE["⚖️ 负载均衡结果:<br/>P0: 2个goroutine<br/>P1: 2个goroutine<br/>P2: 3个goroutine"]
end
classDef balanced fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef reduced fill:#fff9c4,stroke:#f9a825,stroke-width:2px
classDef unchanged fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef result fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
class P0_2,LRQ0_2,CURRENT_2 balanced
class P1_2,LRQ1_2 reduced
class P2_2,LRQ2_2 unchanged
class BALANCE result
步骤6: 阻塞等待或休眠
设计目的: 当所有方法都找不到工作时,让M休眠,避免CPU空转。
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func findrunnable() (gp *g, inheritTime, tryWakeP bool, rnow, pollUntil int64, newWork bool) {
mp := getg().m
pp := mp.p.ptr()
// 最后的手段:阻塞等待工作
if sched.gcwaiting.Load() || pp.runSafePointFn != 0 {
// GC等待或安全点
lock(&sched.lock)
if sched.gcwaiting.Load() || pp.runSafePointFn != 0 {
unlock(&sched.lock)
return gcstopm(), false, false, now, pollUntil, true
}
unlock(&sched.lock)
}
// 所有地方都找不到工作,进入休眠
lock(&sched.lock)
if sched.runqsize != 0 {
gp := globrunqget(pp, 0)
unlock(&sched.lock)
return gp, false, false, now, pollUntil, true
}
// 进入stopm,M休眠
if releasep() != pp {
throw("findrunnable: wrong p")
}
now = pidleput(pp, now)
unlock(&sched.lock)
stopm() // 休眠M直到被唤醒
goto top // 被唤醒后重新开始寻找
}
3.4 Goroutine执行阶段
当调度器找到合适的goroutine后,会调用execute
函数开始执行:
状态转换图 - 执行准备:
graph TB
subgraph "执行前状态 - 准备切换到goroutine"
subgraph "M-P绑定"
M0["🧵 M0<br/>当前在调度器栈"]
P0["📊 P0<br/>已找到G4执行"]
G0["⚙️ G0<br/>系统调度栈"]
M0 -.-> P0
M0 --- G0
end
subgraph "目标Goroutine"
G4["🎯 G4<br/>状态: _Grunnable<br/>等待执行"]
GOBUF["📋 gobuf上下文<br/>sp: 栈指针<br/>pc: 程序计数器<br/>g: goroutine指针"]
G4 --- GOBUF
end
EXECUTE["🚀 execute()函数<br/>准备状态切换"]
EXECUTE -.-> G4
end
classDef machine fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef system fill:#e0e0e0,stroke:#616161,stroke-width:2px
classDef target fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef action fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class M0,P0 machine
class G0 system
class G4,GOBUF target
class EXECUTE action
核心执行源码:
// 源码位置:go/src/runtime/proc.go
func execute(gp *g, inheritTime bool) {
mp := getg().m
// 1. 状态设置
mp.curg = gp // M关联当前goroutine
gp.m = mp // G关联当前M
casgstatus(gp, _Grunnable, _Grunning) // 状态转换
// 2. 时间片和抢占设置
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + _StackGuard
if !inheritTime {
mp.p.ptr().schedtick++
}
// 3. 跳转到goroutine执行
// gogo是汇编实现,完成栈切换和上下文恢复
gogo(&gp.sched)
}
状态转换图 - 执行中:
graph TB
subgraph "执行中状态 - goroutine正在运行"
subgraph "M-P-G绑定"
M0_EX["🧵 M0<br/>执行用户代码"]
P0_EX["📊 P0<br/>绑定到M0"]
G4_EX["🏃 G4<br/>状态: _Grunning<br/>正在CPU上执行"]
M0_EX -.-> P0_EX
M0_EX --- G4_EX
end
subgraph "运行时状态"
STACK["📚 G4的栈<br/>用户代码执行"]
PREEMPT["⏰ 抢占检查<br/>stackguard0监控"]
CPU["💻 CPU<br/>执行G4指令"]
G4_EX --- STACK
STACK -.-> PREEMPT
STACK -.-> CPU
end
MONITOR["👁️ 监控机制<br/>- sysmon定期检查<br/>- 信号抢占<br/>- 栈增长检查"]
MONITOR -.-> PREEMPT
end
classDef running fill:#4caf50,stroke:#2e7d32,stroke-width:3px
classDef monitoring fill:#ff9800,stroke:#f57c00,stroke-width:2px
classDef system fill:#2196f3,stroke:#1976d2,stroke-width:2px
class M0_EX,P0_EX,G4_EX running
class STACK,CPU system
class PREEMPT,MONITOR monitoring
3.5 Goroutine调度切换机制全景解析
Goroutine的调度切换是GMP调度器的核心功能,可能由多种情况触发。每种切换方式都有其特定的设计目的和实现机制。
3.5.1 主动让出 - Gosched()
设计目的: 允许goroutine主动让出CPU时间片,实现协作式调度。
状态转换图 - 主动让出前:
graph TB
subgraph "当前状态 - G4主动调用Gosched()"
subgraph "M-P-G绑定关系"
M0["🧵 M0<br/>执行G4代码"]
P0["📊 P0<br/>状态: _Prunning"]
G4["🏃 G4<br/>状态: _Grunning<br/>调用runtime.Gosched()"]
M0 -.-> P0
M0 --- G4
end
subgraph "本地队列状态"
LRQ["📋 P0本地队列<br/>[G5][G6][G7]<br/>3个等待的goroutine"]
RUNNEXT["🎯 runnext: G8<br/>下一个优先执行"]
P0 --- LRQ
P0 --- RUNNEXT
end
CALL["📞 主动调用<br/>runtime.Gosched()<br/>协作式让出CPU"]
G4 --> CALL
end
classDef running fill:#4caf50,stroke:#2e7d32,stroke-width:3px
classDef waiting fill:#fff9c4,stroke:#f9a825,stroke-width:2px
classDef priority fill:#f8bbd9,stroke:#e91e63,stroke-width:2px
classDef action fill:#ff5722,stroke:#d84315,stroke-width:2px
class M0,P0,G4 running
class LRQ waiting
class RUNNEXT priority
class CALL action
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func Gosched() {
checkTimeouts() // 检查定时器超时
mcall(gosched_m) // 切换到M的g0栈执行调度
}
func gosched_m(gp *g) {
goschedImpl(gp, false) // false表示非抢占式让出
}
// 源码位置:go/src/runtime/proc.go
func goschedImpl(gp *g, preempted bool) {
casgstatus(gp, _Grunning, _Grunnable) // 状态转换
dropg() // 解除M-G绑定
lock(&sched.lock)
globrunqput(gp) // 放入全局队列
unlock(&sched.lock)
schedule() // 重新调度
}
状态转换图 - 主动让出后:
graph TB
subgraph "状态转换 - 完成主动让出"
subgraph "重新调度结果"
M0_NEW["🧵 M0<br/>执行新的goroutine"]
P0_NEW["📊 P0<br/>状态: _Prunning"]
G8_NEW["🏃 G8<br/>状态: _Grunning<br/>从runnext获得执行权"]
M0_NEW -.-> P0_NEW
M0_NEW --- G8_NEW
end
subgraph "全局队列"
GRQ["🌐 全局运行队列<br/>[G4]<br/>G4进入全局队列"]
end
subgraph "本地队列更新"
LRQ_NEW["📋 P0本地队列<br/>[G5][G6][G7]<br/>保持不变"]
RUNNEXT_NEW["🎯 runnext: nil<br/>G8已被执行"]
P0_NEW --- LRQ_NEW
P0_NEW --- RUNNEXT_NEW
end
FAIRNESS["⚖️ 公平调度<br/>G4进入全局队列<br/>等待被其他P窃取<br/>或重新调度"]
GRQ --> FAIRNESS
end
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef global fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef local fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef empty fill:#f5f5f5,stroke:#9e9e9e,stroke-width:2px
classDef concept fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
class M0_NEW,P0_NEW,G8_NEW active
class GRQ global
class LRQ_NEW local
class RUNNEXT_NEW empty
class FAIRNESS concept
3.5.2 系统调用 - 进入内核态
设计目的: 在系统调用期间释放P资源,避免阻塞其他goroutine的执行。
状态转换图 - 系统调用前:
graph TB
subgraph "系统调用前 - G即将进入内核"
M0["🧵 M0<br/>绑定P0和G4"]
P0["📊 P0<br/>状态: _Prunning"]
G4["🏃 G4<br/>调用syscall()"]
M0 -.-> P0
M0 --- G4
SYSCALL["🔄 系统调用<br/>read/write/网络等"]
G4 --> SYSCALL
end
classDef before fill:#ffeb3b,stroke:#f57f17,stroke-width:3px
classDef syscall fill:#ff5722,stroke:#d84315,stroke-width:2px
class M0,P0,G4 before
class SYSCALL syscall
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func reentersyscall(pc, sp, bp uintptr) {
gp := getg()
gp.m.locks++
gp.throwsplit = true // 不得拆分栈
save(pc, sp, bp) // 保存上下文
gp.syscallsp = sp
gp.syscallpc = pc
gp.syscallbp = bp
casgstatus(gp, _Grunning, _Gsyscall) // 状态转换
// 释放P给其他M使用
if sched.sysmonwait.Load() {
systemstack(entersyscall_sysmon)
}
if gp.m.p.ptr().runqhead != gp.m.p.ptr().runqtail {
// 如果本地队列有工作,快速释放P
systemstack(entersyscallblock_handoff)
}
}
状态转换图 - 系统调用中:
graph TB
subgraph "系统调用中 - P被释放,M阻塞"
M0_SYS["🧵 M0<br/>阻塞在内核<br/>执行系统调用"]
P0_FREE["📊 P0<br/>状态: _Pidle<br/>被释放到空闲列表"]
G4_SYS["⏳ G4<br/>状态: _Gsyscall<br/>等待系统调用返回"]
M0_SYS --- G4_SYS
P0_FREE -.-> |"释放给其他M"| OTHER["🧵 其他M<br/>可获取P0"]
KERNEL["🏛️ 内核空间<br/>执行系统调用"]
M0_SYS -.-> KERNEL
end
classDef syscall fill:#ff5722,stroke:#d84315,stroke-width:3px
classDef idle fill:#9e9e9e,stroke:#616161,stroke-width:2px
classDef kernel fill:#673ab7,stroke:#512da8,stroke-width:2px
classDef other fill:#4caf50,stroke:#388e3c,stroke-width:2px
class M0_SYS,G4_SYS syscall
class P0_FREE idle
class KERNEL kernel
class OTHER other
3.5.3 网络I/O阻塞 - netpoll
设计目的: 对网络I/O进行非阻塞处理,将阻塞的goroutine加入网络轮询器管理。
状态转换图 - 网络I/O阻塞前:
graph TB
subgraph "当前状态 - G4执行网络操作"
subgraph "运行状态"
M0["🧵 M0<br/>执行G4"]
P0["📊 P0<br/>状态: _Prunning"]
G4["🏃 G4<br/>调用net.Read()"]
M0 -.-> P0
M0 --- G4
end
subgraph "网络状态"
SOCKET["🔌 TCP Socket<br/>暂无数据可读"]
NETOP["🌐 网络操作<br/>conn.Read(buf)"]
G4 --> NETOP
NETOP --> SOCKET
end
BLOCK["⏸️ 即将阻塞<br/>等待网络数据"]
SOCKET --> BLOCK
end
classDef running fill:#4caf50,stroke:#2e7d32,stroke-width:3px
classDef network fill:#2196f3,stroke:#1976d2,stroke-width:2px
classDef blocking fill:#ff9800,stroke:#f57c00,stroke-width:2px
class M0,P0,G4 running
class SOCKET,NETOP network
class BLOCK blocking
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer,
reason waitReason, traceReason traceBlockReason, traceskip int) {
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason // waitReasonIOWait for network I/O
// 切换到g0栈执行阻塞
mcall(park_m)
}
// 网络轮询器将就绪的goroutine放回运行队列
func netpollready(toRun *gList, pd *pollDesc, mode int32) {
var rg, wg *g
// 将ready的goroutine加入待运行列表
if rg != nil {
netpollunblock(pd, 'r', true)
toRun.push(rg)
}
if wg != nil {
netpollunblock(pd, 'w', true)
toRun.push(wg)
}
}
状态转换图 - 网络I/O阻塞后:
graph TB
subgraph "状态转换 - 网络I/O阻塞处理"
subgraph "Goroutine状态"
G4_WAIT["😴 G4<br/>状态: _Gwaiting<br/>waitReason: IOWait"]
NETPOLL["🌐 网络轮询器<br/>管理阻塞的goroutine"]
G4_WAIT -.-> NETPOLL
end
subgraph "M-P重新调度"
M0_NEW["🧵 M0<br/>执行其他goroutine"]
P0_CONT["📊 P0<br/>继续处理工作"]
G5_RUN["🏃 G5<br/>从本地队列获得执行权"]
M0_NEW -.-> P0_CONT
M0_NEW --- G5_RUN
end
subgraph "网络事件"
EVENT["📡 网络事件就绪<br/>数据到达/连接建立"]
READY["✅ netpoll检测到<br/>G4可以继续执行"]
EVENT --> READY
READY -.-> G4_WAIT
end
EFFICIENCY["⚡ 高效处理<br/>M和P不被阻塞<br/>继续处理其他工作"]
end
classDef waiting fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
classDef netpoll fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:2px
classDef event fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef concept fill:#fff3e0,stroke:#f57c00,stroke-width:2px
class G4_WAIT waiting
class NETPOLL netpoll
class M0_NEW,P0_CONT,G5_RUN active
class EVENT,READY event
class EFFICIENCY concept
3.5.4 定时器超时 - Timer/Sleep
设计目的: 处理时间相关的调度,如time.Sleep、time.After等操作。
状态转换图 - 定时器阻塞前:
graph TB
subgraph "当前状态 - G4调用time.Sleep"
subgraph "执行状态"
M0["🧵 M0<br/>执行G4"]
P0["📊 P0<br/>状态: _Prunning"]
G4["🏃 G4<br/>调用time.Sleep(5*time.Second)"]
M0 -.-> P0
M0 --- G4
end
subgraph "定时器创建"
TIMER["⏰ Timer创建<br/>when: now + 5秒<br/>f: goroutineReady"]
HEAP["📚 P0定时器堆<br/>按when排序"]
G4 --> TIMER
TIMER --> HEAP
end
SLEEP["😴 准备休眠<br/>等待定时器触发"]
TIMER --> SLEEP
end
classDef running fill:#4caf50,stroke:#2e7d32,stroke-width:3px
classDef timer fill:#9c27b0,stroke:#6a1b9a,stroke-width:2px
classDef sleeping fill:#ff9800,stroke:#f57c00,stroke-width:2px
class M0,P0,G4 running
class TIMER,HEAP timer
class SLEEP sleeping
核心源码分析:
// 源码位置:go/src/runtime/time.go
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg()
t := gp.timer
if t == nil {
t = new(timeTimer)
gp.timer = t
}
when := nanotime() + ns
t.timer.modify(when, 0, goroutineReady, unsafe.Pointer(gp), gp.timer.seq)
// 阻塞当前goroutine
gopark(resetForSleep, unsafe.Pointer(gp), waitReasonSleep,
traceBlockSleep, 2)
}
// 定时器到期时的回调函数
func goroutineReady(arg any, _ uintptr, _ int64) {
goready(arg.(*g), 0) // 唤醒goroutine
}
状态转换图 - 定时器唤醒后:
graph TB
subgraph "状态转换 - 定时器唤醒处理"
subgraph "定时器触发"
TRIGGER["⏰ 定时器到期<br/>时间: now >= when<br/>触发goroutineReady"]
CALLBACK["📞 回调执行<br/>goready(gp, 0)"]
TRIGGER --> CALLBACK
end
subgraph "Goroutine唤醒"
G4_READY["😊 G4<br/>状态: _Gwaiting → _Grunnable<br/>重新进入运行队列"]
SCHEDULE["🔄 重新调度<br/>等待获得执行机会"]
CALLBACK --> G4_READY
G4_READY --> SCHEDULE
end
subgraph "执行恢复"
M0_EXEC["🧵 M0<br/>可能执行G4"]
P0_WORK["📊 P0<br/>处理唤醒的goroutine"]
G4_RESUME["🏃 G4<br/>从time.Sleep返回<br/>继续执行后续代码"]
M0_EXEC -.-> P0_WORK
M0_EXEC --- G4_RESUME
end
PRECISION["⏱️ 时间精度<br/>定时器精度取决于<br/>系统时钟和sysmon检查频率"]
end
classDef timer fill:#9c27b0,stroke:#6a1b9a,stroke-width:3px
classDef ready fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
classDef active fill:#ffeb3b,stroke:#f57f17,stroke-width:2px
classDef concept fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
class TRIGGER,CALLBACK timer
class G4_READY,SCHEDULE ready
class M0_EXEC,P0_WORK,G4_RESUME active
class PRECISION concept
3.5.5 Goroutine正常结束 - goexit
设计目的: 处理goroutine函数正常执行完成后的清理和回收工作。
状态转换图 - Goroutine执行完成:
graph TB
subgraph "当前状态 - G4即将结束"
subgraph "执行完成"
M0["🧵 M0<br/>执行G4最后指令"]
P0["📊 P0<br/>状态: _Prunning"]
G4["🏃 G4<br/>函数return<br/>准备退出"]
M0 -.-> P0
M0 --- G4
end
subgraph "栈状态"
STACK["📚 G4栈<br/>包含返回值<br/>即将被回收"]
DEFER["🔄 defer链表<br/>需要执行"]
G4 --- STACK
G4 --- DEFER
end
EXIT["🚪 goexit调用<br/>开始清理流程"]
G4 --> EXIT
end
classDef running fill:#4caf50,stroke:#2e7d32,stroke-width:3px
classDef cleanup fill:#ff9800,stroke:#f57c00,stroke-width:2px
classDef exit fill:#f44336,stroke:#d32f2f,stroke-width:2px
class M0,P0,G4 running
class STACK,DEFER cleanup
class EXIT exit
核心源码分析:
func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g {
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous
}
// 源码位置:go/src/runtime/proc.go
func goexit1() {
// 执行延迟函数
if raceenabled {
racegoend()
}
if traceEnabled() {
traceGoEnd()
}
mcall(goexit0) // 切换到g0栈进行清理
}
func goexit0(gp *g) {
mp := getg().m
casgstatus(gp, _Grunning, _Gdead) // 标记为死亡状态
// 清理goroutine资源
dropg() // 解除M-G绑定
gp.m = nil
locked := gp.lockedm != 0
gp.lockedm = 0
mp.lockedg = 0
// 将G放入P的gFree缓存或全局gFree
gfput(mp.p.ptr(), gp)
// 继续调度其他goroutine
schedule()
}
状态转换图 - Goroutine清理回收:
graph TB
subgraph "状态转换 - Goroutine生命周期结束"
subgraph "清理阶段"
DEFER_EXEC["🔄 执行defer函数<br/>清理资源"]
TRACE_END["📝 结束trace记录<br/>性能统计"]
RACE_END["🔍 结束race检测<br/>并发安全"]
DEFER_EXEC --> TRACE_END
TRACE_END --> RACE_END
end
subgraph "状态转换"
G4_DEAD["💀 G4<br/>状态: _Grunning → _Gdead<br/>生命周期结束"]
DROPG["🔓 dropg()<br/>解除M-G绑定<br/>释放关联关系"]
RACE_END --> G4_DEAD
G4_DEAD --> DROPG
end
subgraph "资源回收"
GFREE["♻️ gfput()<br/>G进入回收池<br/>供后续复用"]
REUSE["🔄 资源复用<br/>栈内存可重用<br/>减少GC压力"]
DROPG --> GFREE
GFREE --> REUSE
end
subgraph "继续调度"
SCHEDULE["🔄 schedule()<br/>寻找下一个goroutine"]
NEXT_G["🏃 执行新的goroutine<br/>保持P的工作连续性"]
REUSE --> SCHEDULE
SCHEDULE --> NEXT_G
end
end
classDef cleanup fill:#ff9800,stroke:#f57c00,stroke-width:3px
classDef dead fill:#f44336,stroke:#d32f2f,stroke-width:3px
classDef recycle fill:#4caf50,stroke:#388e3c,stroke-width:2px
classDef schedule fill:#2196f3,stroke:#1976d2,stroke-width:2px
class DEFER_EXEC,TRACE_END,RACE_END cleanup
class G4_DEAD,DROPG dead
class GFREE,REUSE recycle
class SCHEDULE,NEXT_G schedule
3.5.6 抢占式调度 - 信号/栈增长
设计目的: 防止单个goroutine长时间占用CPU,通过信号机制强制进行调度切换。
状态转换图 - 抢占检测:
graph TB
subgraph "当前状态 - 检测到需要抢占"
subgraph "长时间运行"
M0["🧵 M0<br/>执行G4 > 10ms"]
P0["📊 P0<br/>schedtick无变化"]
G4["🏃 G4<br/>CPU密集计算<br/>未主动让出"]
SYSMON["👁️ sysmon<br/>监控线程<br/>检测抢占条件"]
M0 -.-> P0
M0 --- G4
end
subgraph "抢占触发"
PREEMPT_FLAG["🚨 设置抢占标志<br/>gp.preempt = true<br/>gp.stackguard0 = stackPreempt"]
SIGNAL["📡 发送信号<br/>SIGURG信号<br/>中断当前执行"]
SYSMON --> PREEMPT_FLAG
PREEMPT_FLAG --> SIGNAL
end
CHECK["🔍 抢占检查<br/>函数调用/栈增长时<br/>检查stackguard0"]
SIGNAL --> CHECK
end
classDef running fill:#ff9800,stroke:#f57c00,stroke-width:3px
classDef monitor fill:#673ab7,stroke:#512da8,stroke-width:2px
classDef preempt fill:#f44336,stroke:#d32f2f,stroke-width:3px
classDef check fill:#2196f3,stroke:#1976d2,stroke-width:2px
class M0,P0,G4 running
class SYSMON monitor
class PREEMPT_FLAG,SIGNAL preempt
class CHECK check
核心源码分析:
// 源码位置:go/src/runtime/proc.go
func retake(now int64) uint32 {
n := 0
lock(&allpLock)
for i := 0; i < len(allp); i++ {
pp := allp[i]
if pp == nil {
continue
}
pd := &pp.sysmontick
s := pp.status
sysretake := false
if s == _Prunning || s == _Psyscall {
// 检查是否需要抢占
t := int64(pp.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
} else if pd.schedwhen+forcePreemptNS <= now {
// 超过10ms未调度,强制抢占
preemptone(pp)
sysretake = true
}
}
if sysretake {
n++
}
}
unlock(&allpLock)
return uint32(n)
}
// 抢占指定P上的goroutine
func preemptone(pp *p) bool {
mp := pp.m.ptr()
if mp == nil || mp == getg().m {
return false
}
gp := mp.curg
if gp == nil || gp == mp.g0 {
return false
}
gp.preempt = true
gp.stackguard0 = stackPreempt
// 发送抢占信号
if preemptMSupported && mp.signalPending.CompareAndSwap(0, 1) {
signalM(mp, sigPreempt)
}
return true
}
状态转换图 - 抢占执行:
graph TB
subgraph "状态转换 - 抢占式调度执行"
subgraph "信号处理"
SIGNAL_RECV["📡 接收SIGURG信号<br/>中断当前执行流"]
SIGNAL_HANDLER["🛠️ 信号处理函数<br/>检查抢占标志"]
SIGNAL_RECV --> SIGNAL_HANDLER
end
subgraph "抢占检查"
STACK_CHECK["📚 栈增长检查<br/>stackguard0 == stackPreempt"]
PREEMPT_CHECK["🚨 抢占条件满足<br/>gp.preempt == true"]
SIGNAL_HANDLER --> STACK_CHECK
STACK_CHECK --> PREEMPT_CHECK
end
subgraph "强制调度"
GOSCHEDGUARDED["🔄 goschedguarded()<br/>受保护的调度切换"]
STATE_CHANGE["📊 状态转换<br/>_Grunning → _Grunnable"]
RESCHEDULE["🔄 重新进入调度循环<br/>schedule()"]
PREEMPT_CHECK --> GOSCHEDGUARDED
GOSCHEDGUARDED --> STATE_CHANGE
STATE_CHANGE --> RESCHEDULE
end
subgraph "调度结果"
G4_PREEMPTED["😵 G4<br/>被抢占<br/>进入本地/全局队列"]
G5_RUNNING["🏃 G5<br/>获得执行机会<br/>公平调度实现"]
RESCHEDULE --> G4_PREEMPTED
RESCHEDULE --> G5_RUNNING
end
end
classDef signal fill:#ff5722,stroke:#d84315,stroke-width:3px
classDef check fill:#ff9800,stroke:#f57c00,stroke-width:2px
classDef schedule fill:#2196f3,stroke:#1976d2,stroke-width:2px
classDef result fill:#4caf50,stroke:#388e3c,stroke-width:2px
class SIGNAL_RECV,SIGNAL_HANDLER signal
class STACK_CHECK,PREEMPT_CHECK check
class GOSCHEDGUARDED,STATE_CHANGE,RESCHEDULE schedule
class G4_PREEMPTED,G5_RUNNING result
3.6 调度切换机制总结
六种主要调度切换场景对比:
切换类型 | 触发条件 | 设计目的 | 状态转换 | 性能影响 |
---|---|---|---|---|
主动让出 | runtime.Gosched() | 协作式调度 | _Grunning → _Grunnable | 极低,主动协作 |
系统调用 | 进入内核态 | 释放P资源 | _Grunning → _Gsyscall | 中等,P可被其他M获取 |
网络I/O | socket阻塞 | 异步I/O处理 | _Grunning → _Gwaiting | 低,netpoll高效管理 |
定时器 | time.Sleep/After | 时间调度 | _Grunning → _Gwaiting | 低,定时器堆管理 |
正常结束 | 函数返回 | 资源回收 | _Grunning → _Gdead | 极低,资源复用 |
强制抢占 | 运行时间过长 | 公平调度 | _Grunning → _Grunnable | 中等,信号机制 |
核心调度算法特性:
graph TB
subgraph "GMP调度策略核心特性"
subgraph "高效性 (Efficiency)"
LOCAL["📋 本地队列优先<br/>减少锁竞争"]
LOCKFREE["🔓 无锁操作<br/>原子指令实现"]
CACHE["🏃 缓存友好<br/>局部性原理"]
end
subgraph "公平性 (Fairness)"
GLOBAL61["🌐 每61次检查全局队列<br/>防止饥饿"]
WORKSTEAL["⚖️ 工作窃取<br/>负载均衡"]
PREEMPT10MS["⏰ 10ms抢占<br/>防止长时间独占"]
end
subgraph "响应性 (Responsiveness)"
NETPOLL["🌐 网络轮询<br/>及时响应I/O"]
TIMER["⏰ 定时器管理<br/>精确时间调度"]
SIGNAL["📡 信号抢占<br/>强制调度切换"]
end
subgraph "可伸缩性 (Scalability)"
MULTICORE["🧮 多核支持<br/>P数量=GOMAXPROCS"]
MILLIONS["💫 百万goroutine<br/>轻量级线程"]
ADAPTIVE["🔄 自适应调整<br/>动态M数量"]
end
end
classDef efficiency fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
classDef fairness fill:#fff3e0,stroke:#f57c00,stroke-width:2px
classDef responsiveness fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
classDef scalability fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
class LOCAL,LOCKFREE,CACHE efficiency
class GLOBAL61,WORKSTEAL,PREEMPT10MS fairness
class NETPOLL,TIMER,SIGNAL responsiveness
class MULTICORE,MILLIONS,ADAPTIVE scalability
3.7 调度策略总结
Go的GMP调度器是一个高度优化的用户级线程调度系统,通过多层次的调度策略和六种主要切换机制,实现了高效、公平、响应式的并发执行:
调度策略核心步骤:
- 公平性保证: 每61次调度检查全局队列,防止goroutine饥饿
- 局部性优化: 优先使用本地队列,减少锁竞争和缓存未命中
- 负载均衡: 通过工作窃取算法平衡各P之间的负载
- I/O响应: 及时轮询网络事件,提高I/O密集型应用性能
- 资源管理: 动态调整M的数量,在系统调用时释放P给其他M使用
- 强制抢占: 通过信号机制防止单个goroutine长时间占用CPU
六种调度切换机制:
- 主动让出: 协作式调度,通过
runtime.Gosched()
实现 - 系统调用: 自动释放P资源,避免阻塞其他goroutine
- 网络I/O: 非阻塞网络处理,通过netpoll高效管理
- 定时器: 时间相关调度,支持精确的时间控制
- 正常结束: 资源回收和复用,减少GC压力
- 强制抢占: 信号驱动的抢占式调度,保证公平性
关键性能特征:
- 低延迟: 本地队列无锁操作,调度开销极小
- 高吞吐: 工作窃取保证CPU利用率最大化
- 可伸缩: 支持百万级goroutine并发
- 响应式: 快速响应I/O事件和定时器
- 公平调度: 多种机制防止goroutine饥饿
- 自适应: 根据负载动态调整调度策略
设计优势总结:
- 用户级调度: 避免内核态切换开销,调度成本极低
- 工作窃取: 实现了良好的负载均衡和缓存局部性
- 异步I/O: 网络轮询器使得I/O操作不阻塞CPU资源
- 抢占机制: 信号和栈增长检查保证了调度的公平性
- 资源复用: G、M、P的池化管理减少了内存分配开销
这些特性使得Go语言能够在高并发场景下保持优异的性能表现,为现代分布式系统和微服务架构提供了强大的并发基础。