前言
Go 语言最强大的特性之一就是 goroutine。一个 Go 程序可以轻松创建数十万个 goroutine,它们由 Go 运行时的调度器管理,在少量操作系统线程上高效运行。这一切背后的核心就是 GMP 调度模型。本文将深入 Go 调度器的每一个细节,从数据结构到调度策略,从 Work Stealing 到抢占机制。
调度器演进
从 GM 到 GMP
Go 调度器经历了一次重大架构变革:
GM 模型的问题:
GM 模型 (Go 1.0):┌──────────────────────────────────────────────┐│ Global Queue ││ [G1] [G2] [G3] [G4] [G5] [G6] [G7] ... │└──────────────┬───────────────────────────────┘ │ 全局锁竞争! ┌─────┼─────┐ ▼ ▼ ▼ [M1] [M2] [M3]
问题:1. 全局队列需要加锁, 严重的锁竞争2. M 从全局队列获取 G 时, 所有 M 都在争抢同一把锁3. G 的局部性差: 同一个 G 可能被不同的 M 执行4. 没有本地的缓存, 每次都要访问全局队列GMP 模型的改进:
GMP 模型 (Go 1.1+):┌─────────────────────────────────────┐│ Global Queue ││ [G1] [G2] [G3] [G4] [G5] ... │└─────────────────────────────────────┘
P1 (本地队列) P2 (本地队列) [G10][G11][G12] [G20][G21][G22] │ │ ▼ ▼ [M1] [M2]
优势:1. P 本地队列无锁, 减少竞争2. G 的局部性好: 同一个 P 上的 G 通常在同一个 M 上执行3. Work Stealing: 空闲 P 可以从其他 P 窃取 G4. Hand Off: M 阻塞时 P 可以绑定到新的 M一、核心数据结构
1.1 G (Goroutine)
G 是 goroutine 的运行时表示,源码定义在 src/runtime/runtime2.go:
// src/runtime/runtime2.go - 简化type g struct { // 栈信息 stack stack // 栈范围 [lo, hi] stackguard0 uintptr // 栈溢出检查 stackguard1 uintptr // 栈溢出检查 (C 栈)
// 调度信息 _panic *_panic _defer *_defer m *m // 当前绑定的 M sched gobuf // 保存的调度上下文 param unsafe.Pointer
// 状态 atomicstatus atomic.Uint32 gopc uintptr // 创建者的 PC startpc uintptr // go 语句的 PC
// 抢占相关 preempt bool // 抢占标志 preemptStop bool // 抢占时停止 preemptShrink bool // 抢占时缩小栈
// 等待相关 waiting *sudog // 等待队列 timer *timer
// 栈增长 stktopsp uintptr}栈结构:
Goroutine 栈布局 (动态增长):高地址┌────────────────────────────────┐│ Stack Guard │ ← stackguard0│ (栈溢出检测区域) │├────────────────────────────────┤│ ││ 栈增长空间 ││ (预留给栈增长) ││ │├────────────────────────────────┤ ← stktopsp│ 函数帧 N │├────────────────────────────────┤│ 函数帧 N-1 │├────────────────────────────────┤│ ... │├────────────────────────────────┤│ 函数帧 1 (runtime.main) │├────────────────────────────────┤│ 函数帧 0 (goexit) │ ← sched.sp└────────────────────────────────┘ ← stack.lo低地址G 的状态转换:
G 的完整状态列表:
┌──────────────┬──────────────┬──────────────────────────────────┐│ 状态 │ 值 │ 说明 │├──────────────┼──────────────┼──────────────────────────────────┤│ _Gidle │ 0 │ 刚分配, 尚未初始化 ││ _Grunnable │ 1 │ 在运行队列中等待执行 ││ _Grunning │ 2 │ 正在执行 ││ _Gsyscall │ 3 │ 正在执行系统调用 ││ _Gwaiting │ 4 │ 被阻塞 (chan/IO/timer) ││ _Gdead │ 6 │ 已退出或复用 ││ _Gcopystack │ 8 │ 栈正在被复制/增长 ││ _Gpreempted │ 9 │ 被抢占 (Go 1.14+) │└──────────────┴──────────────┴──────────────────────────────────┘1.2 M (Machine)
M 代表一个操作系统线程:
// src/runtime/runtime2.go - 简化type m struct { g0 *g // M 的特殊 g, 用于调度器栈 curg *g // 当前运行的 G p puintptr // 绑定的 P nextp puintptr // 唤醒时绑定的 P oldp puintptr // 系统调用前的 P
// 线程信息 id int64 procid uint64 // OS 线程 ID osthread bool // 是否为 OS 线程
// 调度信息 spinning bool // 是否正在寻找 G lockedg guintptr // 锁定的 G (LockOSThread) park note // 休眠通知 alllink *m // 全局 M 链表
// 用于 cgo ncgocall uint64 ncgo int32}g0 的作用:
每个 M 都有一个特殊的 g0 (调度栈):┌─────────────────────────────────────┐│ 用户 G 的栈 │ ← curg.stack│ (业务代码, 可动态增长/缩小) │├─────────────────────────────────────┤│ g0 的栈 │ ← g0.stack│ (调度器代码, 固定大小 ~8KB) ││ - schedule() ││ - execute() ││ - gosave() / gogo() ││ - 栈增长 (newstack) ││ - GC 相关操作 │└─────────────────────────────────────┘
切换过程: curg ──gosave()──▶ g0 (保存用户 G 的上下文) g0 ──gogo()───▶ curg (恢复用户 G 的上下文)1.3 P (Processor)
P 是逻辑处理器,是 GMP 模型的核心创新:
// src/runtime/runtime2.go - 简化type p struct { id int32 status uint32 // P 的状态 link puintptr m muintptr // 绑定的 M
// 本地运行队列 (无锁) runqhead uint32 runqtail uint32 runq [256]guintptr // 本地队列 (环形缓冲区) runnext guintptr // 优先运行的 G
// 可用的 G 缓存 gFree struct { gList n int32 }
// 延时执行的 G (timer) timers []*timer numTimers atomic.Int32
// GC 相关 gcAssistTime float64 gcFractionalMarkTime float64}P 的状态转换:
┌──────────────┬──────┬──────────────────────────────────────┐│ 状态 │ 值 │ 说明 │├──────────────┼──────┼──────────────────────────────────────┤│ _Pidle │ 0 │ 空闲, 没有 M 绑定 ││ _Prunning │ 1 │ 被 M 绑定, 正在执行用户代码 ││ _Psyscall │ 2 │ 绑定的 M 在系统调用中 ││ _Pgcstop │ 3 │ 被GC 暂停 ││ _Pdead │ 4 │ 不再使用 (GOMAXPROCS 减小) │└──────────────┴──────┴──────────────────────────────────────┘1.4 三者关系
GMP 关系全景:┌──────────────────────────────────────────────────────────┐│ Global Run Queue ││ [G1] [G2] [G3] [G4] [G5] [G6] ... │└──────────────────────────────────────────────────────────┘ │ │ │ ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ │ P0 │ │ P1 │ │ P2 │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │本地队列│ │ │ │本地队列│ │ │ │本地队列│ │ │ │[G][G] │ │ │ │[G][G] │ │ │ │[G][G] │ │ │ │[G][G] │ │ │ │[G][G] │ │ │ │[G][G] │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ runnext:G│ │ runnext:G│ │ runnext:G│ │ │ │ │ │ │ │ │ │ └────┼─────┘ └────┼─────┘ └────┼─────┘ │ │ │ ▼ ▼ ▼ [M0] [M1] [M2] (OS Thread) (OS Thread) (OS Thread) │ │ │ ▼ ▼ ▼ 执行 curg 执行 curg 执行 curg二、调度流程
2.1 从 go func() 到被调度
当你写下 go func() 时,运行时做了什么?
// 用户代码go hello("world")编译器会将 go 关键字翻译为 runtime.newproc 调用:
// src/runtime/proc.go - 简化func newproc(fn *funcval, argp unsafe.Pointer) { gp := getg() pc := getcallerpc()
// 获取当前 G 所在的 P pp := gp.m.p.ptr()
// 从 P 的空闲列表获取或新建 G newg := gfget(pp) if newg == nil { // 创建新的 G, 初始栈大小 2KB (Go 1.4+) newg = malg(stackMin) casgstatus(newg, _Gidle, _Gdead) allgadd(newg) }
// 设置 G 的入口函数和参数 memmove(unsafe.Pointer(newg.stack.hi-argSize), argp, argSize) gostartcallfn(&newg.sched, fn) newg.gopc = pc
// 将 G 放入运行队列 casgstatus(newg, _Gdead, _Grunnable) runqput(pp, newg, true)
// 如果有其他空闲的 P, 唤醒一个 M 来执行 if mainStarted { wakep() }}完整流程:
2.2 调度核心循环
M 的执行循环在 src/runtime/proc.go 的 schedule 函数中:
// src/runtime/proc.go - 简化func schedule() { mp := getg().m
// 每调度 61 次, 优先从全局队列获取 (防止饥饿) if mp.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp := globrunqget(mp.p.ptr(), 1) unlock(&sched.lock) if gp != nil { execute(gp, inheritTime) return } }
// 1. 从 P 的本地队列获取 gp, inheritTime := runqget(mp.p.ptr())
// 2. 没有可运行的 G, 开始寻找 if gp == nil { gp, inheritTime = findrunnable() }
execute(gp, inheritTime)}findrunnable 查找顺序:
// src/runtime/proc.go - 简化func findrunnable() (gp *g, inheritTime bool) { mp := getg().m pp := mp.p.ptr()
// 1. 本地队列 if gp, inheritTime := runqget(pp); gp != nil { return gp, inheritTime }
// 2. 全局队列 if sched.runqsize > 0 { lock(&sched.lock) gp := globrunqget(pp, 0) unlock(&sched.lock) if gp != nil { return gp, false } }
// 3. 网络轮询器 if netpollinited() { if gp := netpoll(false); gp != nil { // 非阻塞 return gp, false } }
// 4. Work Stealing: 从其他 P 窃取 gp, inheritTime := stealWork(now) if gp != nil { return gp, inheritTime }
// 5. 再次检查全局队列和 netpoll...
// 6. 实在没有 G, 休眠 stopm() // 被唤醒后重新 schedule() return nil, false}三、Work Stealing:窃取机制
3.1 窃取策略
当一个 P 的本地队列和全局队列都没有可运行的 G 时,它会尝试从其他 P 窃取:
窃取算法实现(stealWork):
// src/runtime/proc.go - 简化func stealWork(now int64) (gp *g, inheritTime bool) { pp := getg().m.p.ptr() p2 := randomStealOrder(pp.id) // 随机选择起始 P
for i := 0; i < int(gomaxprocs); i++ { // 跳过自己 if p2 == pp { continue }
// 从 P2 的本地队列窃取一半 gp := runqsteal(pp, p2, true) if gp != nil { return gp, true }
// 还可以窃取 P2 上正在运行的 G (如果它在系统调用中) // 以及 timer
p2 = nextStealOrder(p2) } return nil, false}窃取细节:
P1 本地队列 (环形缓冲区):┌──────────────────────────────────────────────┐│ [G1] [G2] [G3] [G4] [G5] [G6] [_] [_] ... ││ ▲ ▲ ││ runqhead runqtail │└──────────────────────────────────────────────┘
P0 窃取一半 (3 个 G):┌──────────────────────────────────────────────┐│ [G1] [G2] [G3] [_] [_] [_] [_] [_] ... ││ ▲ ▲ ││ runqhead runqtail │└──────────────────────────────────────────────┘
P0 获得: [G4] [G5] [G6]3.2 runqput:放入本地队列
// src/runtime/proc.go - 简化func runqput(pp *p, gp *g, next bool) { // next=true 表示优先执行 (放到 runnext 位置) if next { oldnext := pp.runnext if !pp.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) { // runnext 被其他线程修改了, 重试 } if oldnext != 0 { // 把旧的 runnext 放到队列尾部 runqputslow(pp, oldnext.ptr(), 0) } return }
// 放到本地队列尾部 h := atomic.LoadAcq(&pp.runqhead) t := pp.runqtail if t-h < uint32(len(pp.runq)) { pp.runq[t%uint32(len(pp.runq))].set(gp) atomic.StoreRel(&pp.runqtail, t+1) return }
// 本地队列满了, 移一半到全局队列 runqputslow(pp, gp, 0)}四、Hand Off:系统调用处理
4.1 系统调用时的 M 与 P 分离
当 goroutine 执行系统调用时,M 会被操作系统阻塞。GMP 模型通过 Hand Off 机制,将 P 从阻塞的 M 上分离,绑定到新的 M 继续执行其他 G:
源码实现:
// src/runtime/proc.go - 简化
// 进入系统调用func entersyscall() { mp := getg().m gp := mp.curg
// 保存 G 的调度上下文 save(gp.sched.sp, gp.sched.pc)
// 将 P 的状态设为 Psyscall pp := mp.p.ptr() pp.status = _Psyscall
// 更新系统调用时间戳 pp.syscalltick++}
// 退出系统调用func exitsyscall() { mp := getg().m gp := mp.curg
// 尝试重新获取 P pp := mp.p.ptr() if pp.status == _Psyscall { // 原来的 P 还在, 重新绑定 casgstatus(gp, _Gsyscall, _Grunning) return }
// 原来的 P 已被夺走 // 尝试获取一个空闲的 P pp = pidleget() if pp != nil { mp.p.set(pp) pp.m.set(mp) casgstatus(gp, _Gsyscall, _Grunning) return }
// 没有空闲的 P, 放入全局队列 globrunqput(gp) // M 进入休眠 stopm()}4.2 Hand Off 触发条件
系统监控 sysmon 定期检查处于 _Psyscall 状态的 P:
// src/runtime/proc.go - 简化func retake(now int64) uint32 { n := 0 for i := 0; i < len(allp); i++ { pp := allp[i] s := pp.status
if s == _Psyscall { // 系统调用超过 20μs, 夺取 P t := int64(pp.syscalltick) if runqempty(pp) && sched.nmspinning.Load()+sched.npidle.Load() > 0 { // P 本地队列为空且有其他空闲资源, 不夺取 continue } if now - t > 20*1000 { // 20μs // 原子修改 P 的状态 if pp.casstatus(_Psyscall, _Pidle) { n++ // 把 P 交给其他 M handoffp(pp) } } } } return n}五、抢占机制
5.1 基于协作的抢占 (Go 1.2 - 1.13)
早期的抢占机制依赖函数调用时的栈检查。编译器在每个函数的入口插入栈增长检查代码,如果发现 stackguard0 被设置为 stackPreempt,就会触发抢占:
// 编译器插入的栈检查代码 (伪代码)func someFunction() { // 函数入口: 检查栈 if stackguard0 == stackPreempt { // 被标记为需要抢占 runtime.newstack() // 这里会调用 gosched+0 } // ... 函数体}问题:纯计算密集型循环(无函数调用)无法被抢占:
// 这段代码在 Go 1.13 之前会导致其他 goroutine 饥饿func busyLoop() { for { // 纯计算, 没有函数调用 // 编译器不会插入栈检查代码 // 永远不会被抢占! }}5.2 基于信号的抢占 (Go 1.14+)
Go 1.14 引入了异步抢占(signal-based preemption),解决了协作抢占的缺陷:
源码实现:
// src/runtime/signal_unix.go - 简化func doSigPreempt(gp *g, ctxt *sigctxt) { // 检查是否可以抢占 if wantAsyncPreempt(gp) { // 修改 G 的执行上下文 // 在信号返回后执行 asyncPreempt ctxt.pushCall(funcPC(asyncPreempt)) }}
// src/runtime/preempt.go - 简化func asyncPreempt() { gp := getg() // 保存当前 G 的完整上下文 // 然后调用 gopreempt_m gopreempt_m(gp)}
// src/runtime/proc.go - 简化func gopreempt_m(gp *g) { // 将 G 放回全局运行队列 casgstatus(gp, _Grunning, _Grunnable) globrunqput(gp) // 触发新的调度 schedule()}异步抢占的完整流程:
sysmon 检测到 G 运行超过 10ms │ ▼发送 SIGURG 信号到目标 M │ ▼M 的信号处理器被触发(sighandler → doSigPreempt) │ ▼修改 G 的寄存器上下文(将 PC 指向 asyncPreempt) │ ▼信号处理器返回, M 继续执行 │ ▼G 执行 asyncPreempt() │ ▼保存上下文 → gopreempt_m() │ ▼G 被放入全局队列 │ ▼schedule() 选择下一个 G六、调度器初始化
6.1 schedinit
程序启动时,运行时初始化调度器。入口在 src/runtime/proc.go:
// src/runtime/proc.go - 简化func schedinit() { // 栈、内存分配器、GC 初始化...
// GOMAXPROCS: 默认等于 CPU 核心数 procs := ncpu if n := atoi32(gogetenv("GOMAXPROCS")); n > 0 { procs = n } if procs > _MaxGomaxprocs { procs = _MaxGomaxprocs }
// 创建所有 P procresize(procs)}6.2 procresize
// src/runtime/proc.go - 简化func procresize(nprocs int32) *p { // 1. 初始化所有 P for i := int32(0); i < nprocs; i++ { pp := allp[i] if pp == nil { pp = new(p) } pp.status = _Pgcstop pp.id = i pp.runqtail = 0 pp.runqhead = 0
// 分配 G 的缓存 pp.gFree.n = 0 }
// 2. 多余的 P 上的 G 移到全局队列 for i := nprocs; i < old; i++ { p := allp[i] // 将 P 本地队列的 G 放入全局队列 for p.runqtail != p.runqhead { gp := p.runq[p.runqhead%uint32(len(p.runq))] globrunqput(gp) } // 释放 P p.status = _Pdead }
// 3. 将空闲 P 放入空闲列表 var ppList pListNode for i := nprocs - 1; i >= 0; i-- { p := allp[i] if p.status != _Pidle { continue } pidleput(p) }
return allp[0] // 返回 P0 给 m0}初始化流程:┌─────────────────────────────────────────────────────┐│ schedinit() │├─────────────────────────────────────────────────────┤│ 1. 确定GOMAXPROCS (默认=CPU核心数) ││ 2. procresize(nprocs) ││ ├── 创建 nprocs 个 P ││ ├── P0 绑定 m0 ││ └── P1..Pn 放入空闲列表 ││ 3. 创建 main goroutine ││ └── runtime.main → 用户 main() ││ 4. 启动 sysmon 后台线程 │└─────────────────────────────────────────────────────┘七、全局运行队列与 P 本地队列
7.1 两级队列架构
7.2 队列容量
本地队列: 固定大小 256 个 G (环形缓冲区) runnext: 1 个优先 G 总计: 最多 257 个 G/P
全局队列: 无固定限制 通过 sched.runqsize 和 sched.runq 管理
队列溢出处理: 本地队列满 → 移一半 (128个) 到全局队列7.3 调度优先级
获取 G 的优先级 (从高到低):
1. runnext (最优先执行的 G) ↑ go func() 创建的 G 优先放在这里 │
2. 本地队列 runq[0..255] ↑ FIFO 顺序取出 │
3. 全局队列 (每61次调度检查一次) ↑ 防止全局队列的 G 饥饿 │
4. netpoll (网络就绪的 G) │
5. Work Stealing (从其他 P 窃取) │
6. 休眠八、系统监控 (sysmon)
8.1 sysmon 的职责
sysmon 是一个不需要 P 就能运行的后台线程(M),负责多项监控任务:
// src/runtime/proc.go - 简化func sysmon() { // sysmon 不绑定 P, 独立运行 idleshift := 0 idle := 0 delay := uint32(0)
for { // 动态调整休眠时间 if idle == 0 { delay = 20 // 初始 20μs } else if idle > 50 { delay *= 2 // 最长 10ms } if delay > 10*1000 { delay = 10 * 1000 } usleep(delay)
// 1. 释放闲置超过 5 分钟的 span (内存回收) if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() { gcStart(t) }
// 2. 检查是否需要强制 GC lastgc := sched.lastgc.Load() if lastgc != 0 && now-lastgc > forcegcperiod { // 2 分钟未触发 GC, 强制触发 forcegc.idle = false wakep() }
// 3. 网络轮询 if netpollinited() { gp := netpoll(false) injectglist(gp) }
// 4. 抢占长时间运行的 G retake(now)
// 5. 唤醒沉睡的 M if sched.npidle.Load() != 0 && sched.nmsys.Load() != 0 { wakep() } }}8.2 retake 抢占
retake 负责两件事:夺取长时间系统调用的 P,和抢占长时间运行的 G:
retake 检查逻辑:┌──────────────────────────────────────────┐│ for 每个 P: ││ if P.status == _Psyscall: ││ if 系统调用 > 20μs: ││ handoffp(P) → 夺取 P 给其他 M ││ ││ if P.status == _Prunning: ││ if G 运行 > 10ms: ││ preemptone(P) → 发送 SIGURG 信号 │└──────────────────────────────────────────┘九、网络轮询器 (netpoller)
9.1 netpoll 与调度器的协作
Go 的网络 I/O 使用非阻塞 I/O + epoll/kqueue 实现。当 goroutine 执行网络 I/O 时,如果数据没准备好,goroutine 会被挂起,等到数据就绪后再被唤醒:
9.2 netpoll 实现原理
网络 I/O 流程:┌────────────────────────────────────────────────────┐│ G1: conn.Read(buf) ││ ├── 设置 fd 为非阻塞 ││ ├── read() → EAGAIN (数据未就绪) ││ ├── epoll_ctl(EPOLL_CTL_ADD, fd, EPOLLIN) ││ ├── gopark(netpollblock) ││ │ └── G1 状态: _Gwaiting ││ └── 等待... ││ ││ [内核] 数据到达, 触发 epoll 事件 ││ ││ sysmon / schedule: ││ ├── netpoll(false) ││ │ └── epoll_wait() → 返回就绪的 fd 列表 ││ ├── 找到对应的 G1 ││ └── goready(G1) → 放入运行队列 ││ ││ G1 被调度: ││ └── 继续执行 read() → 成功读取数据 │└────────────────────────────────────────────────────┘十、调度器性能分析
10.1 调度延迟追踪
Go 提供了调度器追踪工具:
# 开启调度器追踪GODEBUG=schedtrace=1000 ./myapp
# 输出示例 (每秒一次):# SCHED 1000ms: gomaxprocs=8# threads=12 spinning=0 idle=0# P0: runq=3 [G1 G2 G3] gfreecnt=5# P1: runq=1 [G4] gfreecnt=2# ...# global runq: 10# idleprocs=0 idlethreads=210.2 常见性能问题
┌───────────────────┬────────────────────┬──────────────────────────┐│ 问题 │ 现象 │ 解决方案 │├───────────────────┼────────────────────┼──────────────────────────┤│ G 数量过多 │ 调度开销大 │ 使用 worker pool 限制 ││ G 阻塞在系统调用 │ M 被占用 │ Hand Off 自动处理 ││ 锁竞争 │ M 自旋等待 │ 减少 shared state ││ GOMAXPROCS 过小 │ CPU 利用率低 │ 增大到 CPU 核心数 ││ 内存不足 │ GC 频繁,暂停长 │ 减少内存分配 │└───────────────────┴────────────────────┴──────────────────────────┘10.3 GOMAXPROCS 调优
import "runtime"
func init() { // 默认值: CPU 核心数 // CPU 密集型: 设置为 CPU 核心数 // I/O 密集型: 可以适当增大 runtime.GOMAXPROCS(runtime.NumCPU())}GOMAXPROCS 对调度的影响:
GOMAXPROCS=1: 只有 1 个 P 所有 G 串行执行 适合调试竞态条件
GOMAXPROCS=CPU核心数 (默认): 充分利用多核 Work Stealing 更高效
GOMAXPROCS > CPU核心数: 可能增加上下文切换开销 通常不推荐总结
GMP 模型全景
核心要点
- GMP 模型:G (goroutine)、M (OS 线程)、P (逻辑处理器) 三层抽象
- Work Stealing:空闲 P 从忙碌 P 窃取 G,均衡负载
- Hand Off:系统调用阻塞 M 时,P 绑定到新 M 继续工作
- 两级队列:P 本地队列(无锁)+ 全局队列(有锁),减少竞争
- 异步抢占:基于 SIGURG 信号的抢占解决了协作抢占无法处理纯计算循环的问题
- sysmon:后台监控线程负责抢占、GC 触发、netpoll、资源回收
常见问题
Q1:goroutine 和线程的区别是什么?
goroutine 是用户态的轻量级线程。创建一个 goroutine 只需要 2KB 栈空间(线程通常 1-8MB),goroutine 的创建、切换和销毁都在用户态完成,不需要陷入内核。Go 调度器将 M 个 goroutine 调度到 N 个 OS 线程上执行(M
Q2:为什么 P 的本地队列大小是 256?
这是一个经验值。太小的队列会导致频繁访问全局队列(锁竞争),太大的队列会浪费内存并增加 Work Stealing 的开销。256 在绝大多数场景下能提供良好的平衡。
Q3:什么时候应该用 runtime.LockOSThread()?
当你需要将 goroutine 绑定到特定的 OS 线程时使用。典型场景:OpenGL/Cocoa 等 GUI 库要求在特定线程操作;使用线程局部存储 (TLS) 的 C 库 (通过 cgo 调用)。调用后,这个 goroutine 会一直绑定到当前 M,直到调用 UnlockOSThread。
Q4:Go 调度器如何保证公平性?
三个机制保证公平:1) 每 61 次调度优先从全局队列取 G,防止全局队列饥饿;2) 基于信号的异步抢占确保每个 G 最多运行 10ms 就让出 CPU;3) sysmon 后台线程定期检查并触发抢占。
Q5:sysmon 不绑定 P,为什么还能运行?
sysmon 运行在一个专用的 M 上,这个 M 不绑定 P。sysmon 只执行一些轻量的管理操作(检查时间戳、发送信号),不执行用户代码,所以不需要 P。M 在没有 P 的情况下可以执行一些有限的运行时代码。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






