“goroutine 很轻”是 Go 的卖点。轻到什么程度?这篇不说“几 KB”这种概数,而是真的创建几万到 50 万个,量内存、创建时间、切换时间,再拿 OS 线程做对照,最后看线程数的上限怎么表现。
10000 个 goroutine(都阻塞在 channel 上):NumGoroutine = 10001;创建 + 就绪 16 ms(每个 1631 ns)
StackInuse +20 MiB;HeapInuse +6 MiB;MSpanInuse +0 MiB;Sys +30 MiB;进程最大 RSS 增加 27 MiB
折算每个 goroutine:栈 2058 字节 + 堆 671 字节(g 结构体等)+ span 元数据 26 字节;Sys 合计 3131 字节
100000 个 goroutine(都阻塞在 channel 上):NumGoroutine = 100001;创建 + 就绪 103 ms(每个 1034 ns)
StackInuse +196 MiB;HeapInuse +63 MiB;MSpanInuse +2 MiB;Sys +263 MiB;进程最大 RSS 增加 262 MiB
折算每个 goroutine:栈 2050 字节 + 堆 665 字节(g 结构体等)+ span 元数据 23 字节;Sys 合计 2760 字节
500000 个 goroutine(都阻塞在 channel 上):NumGoroutine = 500001;创建 + 就绪 448 ms(每个 897 ns)
StackInuse +977 MiB;HeapInuse +317 MiB;MSpanInuse +11 MiB;Sys +1311 MiB;进程最大 RSS 增加 1309 MiB
折算每个 goroutine:栈 2049 字节 + 堆 665 字节(g 结构体等)+ span 元数据 23 字节;Sys 合计 2750 字节栈是大头(2 KiB 起步),另有 650 字节左右的 g 结构体及运行时元数据,合计约 2.8 KB。规模对线性:10 万个 262 MiB,50 万个 1.3 GiB。创建加就绪的平均耗时从 0.8 µs 涨到 1.1 µs,说明规模大了之后分配与调度的开销在增加(没有查具体原因)。
go func(){ wg.Done() }() 创建并跑完:中位 341 ns/个(5 轮:330 / 332 / 341 / 370 / 366);对照:无缓冲 channel 一次发送加接收 131 ns创建并跑完一个最小的 goroutine 约 346 ns,约等于三次 channel 通信(131 ns 一次)。所以“每个请求一个 goroutine”在 Go 里是合理的默认,开销是亚微秒级。
GOMAXPROCS= 1:两个 goroutine 经无缓冲 channel 来回一次(两次切换):中位 189 ns GOMAXPROCS= 2:两个 goroutine 经无缓冲 channel 来回一次(两次切换):中位 246 ns GOMAXPROCS=10:两个 goroutine 经无缓冲 channel 来回一次(两次切换):中位 254 ns
两个 goroutine 经无缓冲 channel 来回一次(两次切换)只要 189 ns(P=1)。有意思的是 P 更多时反而更慢(240–254 ns),我猜是强同步的来回要跨 P 唤醒对方,这个解释没有做进一步验证。
1000 个 LockOSThread 的 goroutine(各占一个 OS 线程):创建 21 ms(每个 22 µs);进程最大 RSS 增加 56 MiB(每个线程约 57 KiB) 2000 个 LockOSThread 的 goroutine(各占一个 OS 线程):创建 131 ms(每个 66 µs);进程最大 RSS 增加 111 MiB(每个线程约 57 KiB) 4000 个 LockOSThread 的 goroutine(各占一个 OS 线程):创建 487 ms(每个 122 µs);进程最大 RSS 增加 221 MiB(每个线程约 57 KiB) 本机对单个进程线程数的限制:4096(kern.num_taskthreads)
让每个 goroutine 占住一个 OS 线程(LockOSThread),创建一个要 22 µs 起,线程多了还会变慢(4000 个时 122 µs),比 goroutine 慢约两个数量级;每个线程光已驻留的内存就约 57 KiB(RSS,不含未触碰的虚拟栈),本机对单进程线程数的上限是 4096。这些数字是 macOS 上的,Linux 上不同。
退出码 2;SetMaxThreads(500) 返回旧值: 10000 / runtime: program exceeds 500-thread limit / fatal error: thread exhaustion / runtime stack:
Go 运行时自己也限制线程数(默认 10000),超出时 fatal error: thread exhaustion。普通的 goroutine 不占线程,只有阻塞在系统调用、cgo 或 LockOSThread 里的才会占——所以几十万个 goroutine 没问题,几千个阻塞在 cgo 里的就可能出问题(见 cgo 一篇)。