goroutine 与操作系统线程有什么区别?
结论先行
goroutine 是 Go 运行时在用户态自己调度的“轻量级协程”,运行在少量 OS 线程之上(N:M 模型),初始栈仅约 2KB 且可动态扩缩;线程由内核调度、栈通常以 MB 计且固定。因此 goroutine 的创建与切换成本比线程低一到两个数量级,单个进程轻松承载数十万 goroutine,而线程数千个就吃力。
要点(对比表)
| 维度 | goroutine | OS 线程 |
|---|---|---|
| 调度方 | Go 运行时(用户态) | 操作系统内核 |
| 初始栈 | 约 2KB,动态增长(上限约 1GB) | 通常 1~8MB,基本固定 |
| 切换代价 | 纯用户态保存/恢复,约百纳秒到微秒级 | 内核上下文切换,微秒级以上 |
| 创建成本 | 低(几 KB 内存加调度器结构) | 高(系统调用加内核栈) |
| 可承载规模 | 单进程数十万没问题 | 数千就吃紧 |
| 阻塞语义 | 阻塞于 channel/网络时挂起,不占线程 | 阻塞会占住内核线程 |
要点(为什么轻量)
- N:M 模型:海量 G 映射到 GOMAXPROCS 个逻辑处理器与少数 M 上执行,详见“GMP 调度模型”一题。
- 让出与抢占:channel、锁、网络轮询等阻塞点主动挂起(gopark);长时间运行的 G 会被 sysmon 抢占,避免饿死其他任务。
- 系统调用处理:真正阻塞 OS 的调用会让对应 M 与 P 解绑,必要时另起 M,保证调度不因一个 syscall 停摆。
- goroutine 也非零成本:栈内存、调度开销、GC 压力都在,数量失控或泄漏照样拖垮进程。
示例
var wg sync.WaitGroup
for i := 0; i < 100_000; i++ { // 十万 goroutine 依然可行
wg.Add(1)
go func(i int) {
defer wg.Done()
_ = i
}(i)
}
wg.Wait()
常见追问/记忆点
- 追问:goroutine 一定比线程快吗?答:单看切换成本是;但并行度受 CPU 核数与 GOMAXPROCS 限制,阻塞型任务收益最大。
- 追问:为什么叫“有栈协程”?答:每个 G 有独立栈、可被抢占、可在任意阻塞点让出,比无栈协程更通用。
- 追问:goroutine 会自己销毁吗?答:函数返回即结束;卡在阻塞点不退出就构成泄漏,需要取消信号配合。
- 记忆:用户态调度 + 小栈动态伸缩 = 高并发低成本的根源。