Skip to main content

Go: goroutine 协程

📅 2026-02-11 ✏️ 2026-03-21 Inside Go GO CS
go version # go version go1.25.6

1 · goroutine 协程#

执行任务单位(用户态轻量线程),由runtime进行调度

  1. 有自己的栈,比较轻量(2kb)
  2. 栈大小不够时,进行扩容
  3. 需要系统线程执行,绑定系统线程M
  4. 调度的时候,有状态:运行中、阻塞中、等待中…
  5. 需要进行系统调用时,保存相关信息
  • Goroutine 和 Thread 区别
维度GoroutineThread
内存消耗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的一帧,用gostartcallfnfn当成被调用者塞进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