goroutine 与操作系统线程有什么区别?

结论先行

goroutine 是 Go 运行时在用户态自己调度的“轻量级协程”,运行在少量 OS 线程之上(N:M 模型),初始栈仅约 2KB 且可动态扩缩;线程由内核调度、栈通常以 MB 计且固定。因此 goroutine 的创建与切换成本比线程低一到两个数量级,单个进程轻松承载数十万 goroutine,而线程数千个就吃力。

要点(对比表)

维度goroutineOS 线程
调度方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 会自己销毁吗?答:函数返回即结束;卡在阻塞点不退出就构成泄漏,需要取消信号配合。
  • 记忆:用户态调度 + 小栈动态伸缩 = 高并发低成本的根源。
笔记加载中…