设为物理核数更优,因逻辑核数会加剧缓存污染与调度争抢;实测可提升5–15%吞吐,混i/o服务除外。
直接用 go func() 启动上万 goroutine 处理 cpu 密集任务,不仅不会提升吞吐,反而会让所有核都卡在调度抖动和缓存失效上——这不是并发不足,是调度失配。
为什么 GOMAXPROCS 设对了还是压不满多核
默认 runtime.GOMAXPROCS() 返回的是逻辑核数(比如 8 核 16 线程 → 设为 16),但纯 CPU 密集型任务跑满 16 个 P,会加剧 L3 缓存污染和调度器队列争抢。实测中,设为物理核数(如 8)常提升 5–15% 吞吐。
-
/proc/cpuinfo查物理核数,或用gopsutil库的CPU.Counts(true) - 混 I/O 的服务(如 HTTP + 计算)可保留逻辑核数;纯数值计算建议降为物理核数
- 运行中反复调用
runtime.GOMAXPROCS(n)会重置调度器状态,引发短暂抖动 - 加
GODEBUG=schedtrace=1000启动,观察每秒活跃 P 数是否稳定
goroutine 泛滥导致 CPU 利用率反降
把 10 万个矩阵乘法扔进 10 万个 go func(),结果比单 goroutine 还慢——因为上下文切换、栈分配、GC 扫描全来了。CPU 密集任务不需要“多”,需要“稳且准”。
- 用固定 worker 池替代泛滥 goroutine:
workerCount = runtime.GOMAXPROCS(0)或略高(4–12) - 每个 worker 用
for job := range jobs持续取任务,避免 goroutine 频繁创建销毁 - 切分任务按数据块(如数组下标区间),而非按条目;8 核就分 8 块,每块由一个 goroutine 独占计算
- 热循环里禁用
fmt.Sprintf、strconv.Itoa;整数转字符串用strconv.AppendInt(dst, n, 10)
如何规避 GC 和隐性同步拖垮 CPU
top 显示 CPU 利用率低,但 pprof 显示大量时间花在 runtime.mallocgc 或 runtime.semawakeup 上,就是信号:不是没干活,是被 GC 或锁卡住了。
- 高频分配小对象(如
[]byte、临时结构体)用sync.Pool,但注意 Pool 不适合 >2KB 的大对象 - map 并发写入会 panic;只读多写少也优先用
sync.Map或分片 map + 局部聚合 - 计数器类场景用
atomic.AddInt64,别用 mutex 包一层再 ++ - 用
go tool trace看 GC 频率和 STW 时间,确认是否成为隐性瓶颈
网络 IO 密集型场景下多核 CPU 利用率上不去的根本原因
Go 的网络轮询器(netpoller)是全局唯一的,底层封装 epoll/kqueue/IOCP,由单个 OS 线程驱动。这意味着:哪怕你启动 100 个 goroutine 调用 http.Server.Serve(),所有 epoll_wait 仍集中在单个线程上,CPU 只有一核忙,其余闲置。
-
runtime.LockOSThread()在这里不仅无效,反而有害——它强制绑定线程,却无法绕过 netpoller 单点瓶颈 - 正确解法是多进程部署:用
SO_REUSEPORT(Linux 3.9+)让多个 Go 进程监听同一端口 - 每个进程绑定专属 NUMA 节点(如
numactl --cpunodebind=0 --membind=0 ./server -port=12345) - 前端用 nginx / HAProxy 分流,避免跨 NUMA 内存访问延迟
真正难的不是开多少 goroutine,而是让每个 P 上的任务密度足够、缓存友好、无 GC 干扰、不跨 NUMA——这些细节一漏,多核就形同虚设。











