为什么 Goroutines 可扩展:栈增长、编译器技巧和上下文切换
抱歉,我需要您提供要翻译的具体文本内容(文章正文)。请把您想要翻译的文字粘贴在这里,我会按照要求保留源链接并进行简体中文翻译。
C++ 与 Java 中的线程开销
在上述语言中,线程是一种并发手段,但在上下文切换时会消耗大量 CPU 时间,并且在创建时会占用相对巨大的内存。单个线程通常会预留 ~1 MiB 的栈空间。因此,生成 100 000 个线程大约需要 100 GiB 的 RAM,这对大多数软件项目来说并不经济可行。
为了保持并发,CPU 通常使用时间片轮转(time‑slicing)来给每个线程分配相同数量的 CPU 周期。在此过程中,CPU 必须执行上下文切换,这相当昂贵:
- 当前线程的状态保存在其 TCB(线程控制块)中。
- 新线程的 TCB 被加载到内存中。
- 上下文切换会破坏缓存局部性,导致频繁的 L1/L2 缓存未命中。
当线程数量达到数千时,CPU 花在切换上下文的时间会超过实际执行代码的时间。
Source: …
Goroutine 如何实现优化
Goroutine 是由 Go 运行时在 用户空间 完全管理的“轻量级线程”,而不是由操作系统内核管理。
内存效率
- 标准的 OS 线程会预留固定的 1 MiB 栈。
- Goroutine 则只从 2 KiB 的栈开始。

计算方式: 2 KiB 大约是 1 MiB 的 0.2 %。
影响: 与其在几千个线程上受限,你可以在普通笔记本上轻松生成 数百万 个 goroutine,而不会耗尽内存。
“无限”栈
与在创建时就确定固定栈大小的 OS 线程不同,goroutine 的栈是 动态 的:
- Goroutine 从 2 KiB 栈开始。
- 当空间不足时,运行时会分配更大的段(通常是当前大小的两倍),并把栈迁移过去。

- OS 线程限制: 固定(≈1–8 MiB)。达到上限会导致崩溃。
- Goroutine 限制: 动态(在 64 位系统上可达约 1 GiB)。
因此,在实际使用中,goroutine 的递归深度只受可用内存限制,而 OS 线程则受其最初预留的栈大小限制。
更快的上下文切换

OS 线程和 goroutine 在暂停时都需要保存状态,但代价差别巨大:
| OS 线程切换 | Goroutine 切换 | |
|---|---|---|
| 典型延迟 | ~1–2 µs | ~200 ns(≈快 10 倍) |
| 保存的内容 | 所有 CPU 寄存器(包括重量级的 FP/AVX 寄存器)→TCB | 仅 3 个寄存器(PC、SP、DX)→一个小的 Go 结构体 (g) |
| 发生位置 | 内核模式(陷阱) | 用户空间(运行时) |
| 缓存影响 | 刷新缓存,失去局部性 | 缓存保持热度,局部性得以保留 |
由于 goroutine 的切换停留在用户空间,开销几乎可以忽略不计。
Goroutine 栈分配是如何工作的
Go 编译器会在每个函数的开头插入一个 函数序言。该序言执行以下检查:
- 比较 当前栈指针 (SP) 与称为 栈守卫 的限制。
- 如果剩余空间不足,则跳转到
runtime.morestack。 runtime.morestack为栈分配一个更大的段(通常是当前大小的 2 倍)。- 运行时 复制 现有栈内容到新段,并 调整所有指针 使其指向新的地址。
- 执行在更大的栈上继续。
示例
package main
import "fmt"
func main() {
fmt.Println("Hello Ayush")
}
使用 -gcflags -S 选项运行编译器会显示 main.main 的生成汇编:
main.main STEXT size=83 args=0x0 locals=0x40 funcid=0x0 align=0x0
0x0000 00000 (/Users/ayushanand/concurrency/main.go:7) TEXT main.main(SB), ABIInternal, $64-0
0x0000 00000 (/Users/ayushanand/concurrency/main.go:7) CMPQ SP, 16(R14) // compare SP with stack guard
0x0004 00004 (/Users/ayushanand/concurrency/main.go:7) PCDATA $0, $-2
0x0004 00004 (/Users/ayushanand/concurrency/main.go:7) JLS 76 // jump to morestack if SP ) NOP
0x002d 00045 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) LEAQ go:itab.*os.File,io.Writer(SB), AX
0x0034 00052 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) LEAQ main..autotmp_8+40(SP), CX
0x0039 00057 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) MOVL $1, DI
0x003e 00062 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) MOVQ DI, SI
0x0041 00065 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) PCDATA $1, $0
0x0041 00065 (/usr/local/Cellar/go/1.25.4/libexec/src/fmt/print.go:314) CALL fmt.Fprintln(SB)
0x0046 00070 (/Users/ayushanand/concurrency/main.go:9) ADDQ $56, SP
0x004a 00074 (/Users/ayushanand/concurrency/main.go:9) POPQ BP
0x004b 00075 (/Users/ayushanand/concurrency/main.go:9) RET
0x004c 00076 (/Users/ayushanand/concurrency/main.go:9) NOP
0x004c 00076 (/Users/ayushanand/concurrency/main.go:7) PCDATA $1, $-1
0x004c 00076 (/Users/ayushanand/concurrency/main.go:7) PCDATA $0, $-2
0x004c 00076 (/Users/ayushanand/concurrency/main.go:7) CALL runtime.morestack_noctxt(SB)
0x0051 00081 (/Users/ayushanand/concurrency/main.go:7) PCDATA $0, $-1
0x0051 00081 (/Users/ayushanand/concurrency/main.go:7) JMP
可以看到用于检查栈大小的汇编代码。
结束语
Goroutine 并不是“线程但更小”。它们代表了对并发管理的根本性重新思考。通过将栈管理从操作系统内核移到 Go 运行时,我们获得了:
- 大规模可伸缩性: 从 100 k 限制提升到数百万的 goroutine。
- 动态内存: 按实际使用付费(≈ 2 KB),而不是按可能使用付费(≈ 1 MB)。
- 低延迟: 上下文切换快约 10 倍。
下次你键入 go func() 时,请记住:背后有一个只有 2 KB 的小栈和一个智能编译器在工作,使其看起来是“无限”的。