Go: goroutine 协程
go version # go version go1.25.6
1 · goroutine 协程#
执行任务单位(用户态轻量线程),由runtime进行调度
- 有自己的栈,比较轻量(2kb)
- 栈大小不够时,进行扩容
- 需要系统线程执行,绑定系统线程M
- 调度的时候,有状态:运行中、阻塞中、等待中…
- 需要进行系统调用时,保存相关信息
- Goroutine 和 Thread 区别
| 维度 | Goroutine | Thread |
|---|---|---|
| 内存消耗 | 2KB,不够自动扩容 | 1MB,被guard page隔离 |
| 创建销毁 | 用户级,GO Runtime管理 | 内核级,与OS打交道 |
| 切换成本 | 200ns,3个寄存器(PC、SP、BP) | 1000-1500ns,保存各种寄存器 |
总结: goroutine是轻量化thread,在创建销毁、切换、内存消耗上优势显著。
1.1 · 生命周期
1.2 · g 定义#
定义:包含栈、调度信息、状态等
// g 的定义
type g struct {
// 1. 栈相关:执行现场
// stack 表示栈内存
stack stack
// 用于检查是否栈增长
stackguard0 uintptr
// 强制走 morestack?
stackguard1 uintptr
// 2. 调度相关(M 现场切换)
// m 表示当前绑定的操作系统线程
m *m // current m; offset known to arm liblink
// sched 用于保存G的执行现场,用于挂起/恢复这个g
sched gobuf
// g 的状态
atomicstatus atomic.Uint32
// 调度队列里的下一个g(空间局部性)
schedlink guintptr
// 3. 阻塞相关
waitsince int64 // approx time when the g become blocked
waitreason waitReason // if status==Gwaiting
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
// 4. 异常相关
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
// 5. 抢占相关
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
preemptStop bool // transition to _Gpreempted on preemption; otherwise, just deschedule
preemptShrink bool // shrink stack at synchronous safe point
// 6. 系统调用相关
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
syscallbp uintptr // if status==Gsyscall, syscallbp = sched.bp to use in fpTraceback
stktopsp uintptr // expected sp at top of stack, to check in traceback
// 其他字段...
}
1.3 · newproc#
核心:在栈顶造好返回地址 = goexit的一帧,用gostartcallfn把fn当成被调用者塞进sched.pc,这样新g第一次被调度时从fn开始跑,fn返回时自然回到goexit,调度下一个g执行。
创建 runtime.newproc -> newproc1
NOTE: newproc1 会在系统栈上被调用
// 创建一个g
func newproc(fn *funcval) {
// 1. 获取当前g
gp := getg()
pc := sys.GetCallerPC()
// 2. 在系统栈(非goroutine栈)上执行
systemstack(func() {
// 3. 创建新的g
newg := newproc1(fn, gp, pc, false, waitReasonZero)
// 4. 获取当前p
pp := getg().m.p.ptr()
// 5. 把g放入队列(可能是p的本地队列,也可能是全局队列)
runqput(pp, newg, true)
if mainStarted {
wakep()
}
})
}
func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g {
// 1. 获取m和p
// 从p获取一个新 g 或分配一个新 g
mp := acquirem() // disable preemption because we hold M and P in local vars.
pp := mp.p.ptr()
newg := gfget(pp)
if newg == nil {
newg = malg(stackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
// 2. 栈顶预留多4个指针的空间,然后对齐
// 4个指针用于:伪造的调用帧(含返回到 goexit 的约定
totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
totalSize = alignUp(totalSize, sys.StackAlign)
sp := newg.stack.hi - totalSize
// 3. 调度上下文
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
// 设成"从 goexit 里某条指令之后,对应Nop?"开始(这样 traceback 会把 goexit 当成调用者)
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
// 栈顶减一个指针,把pc放进去,然后把fn作为新pc
// 和上面一起等价于:模拟「goexit 里已经执行了 CALL fn」,现在正要执行 fn 的第一条指令
gostartcallfn(&newg.sched, fn)
// 4. 记"谁创建的"、启动地址
newg.parentGoid = callergp.goid
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
// ....
return newg
}
2 · 特殊函数
2.1 · execute#
开始执行当前g
g 状态从 _Grunnable -> _Grunning
// 在当前 M(OS 线程)上执行指定的 goroutine gp,且该函数不会返回(执行权会切换到 gp)
func execute(gp *g, inheritTime bool) {
// ...
// 最终调用 gogo,执行权切换到了gp
gogo(&gp.sched)
}
gogo 把执行流从”调度器”切到某个 goroutine 之前保存的现场(栈、寄存器、PC)
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
// 要恢复的 goroutine,放DX
MOVQ gobuf_g(BX), DX
// 读 *g
// 0(DX) 是 *DX,即读取 g 结构体的第一个字段
// 这里主要是空指针检查:如果 DX == nil,访问 0(DX) 会触发段错误,从而快速失败
MOVQ 0(DX), CX // make sure g != nil
JMP gogo<>(SB)
TEXT gogo<>(SB), NOSPLIT, $0
// 取线程本地存储
// CX = TLS 地址(宏展开为 MOVQ TLS, CX)
get_tls(CX)
// TLS 里的 current g 设为要执行的 goroutine
// g(CX) 是宏,展开为 0(CX),表示 TLS 结构体中偏移 0 的字段(g 字段)
// 把 DX(g 指针)写入 TLS 的 g 字段
MOVQ DX, g(CX)
// 在 amd64 上 R14 固定表示“当前 g”
MOVQ DX, R14 // set the g register
// 恢复栈和帧
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
// 清空 gobuf 里已恢复的字段,避免被 GC 误认为仍被引用
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
// 跳转到 goroutine
MOVQ gobuf_pc(BX), BX
JMP BX
2.2 · gopark#
挂起当前g
g 状态从 Grunning → _Gwaiting
// 把当前 goroutine 挂起,并在系统栈上执行 unlockf;unlockf 返回 false 时 goroutine 会恢复
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int)
2.3 · goready#
标记当前 g 为可运行状态
g 状态从 _Gwaiting → _Grunnable
// 把已经 park 的 goroutine 标记为“可运行”并放进调度队列,让其可以被再次调度执行
func goready(gp *g, traceskip int)
2.4 · goexit#
goexit是:每个 goroutine 的调用栈最顶上的返回桩(return stub)
创建新 g 的时候,会构建伪调用帧(入口函数是由 goexit 调用),在g结束的时候,返回到goexit,而goexit最终会调用 schedule 进行调度
goexit -> goexit1 -> goexit0 -> schedule
为什么要这么设计?见栈回溯
// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME|NOFRAME,$0-0
// NOTE: 第一行这里会变成对goroutine的调用
BYTE $0x90 // NOP
CALL runtime·goexit1(SB) // does not return
// traceback from goexit1 must hit code range of goexit
BYTE $0x90 // NOP