goroutine 默认不并行,因 go 运行时初始 gomaxprocs=1,所有 goroutine 在单个 p 上协作调度;需显式调用 runtime.gomaxprocs(runtime.numcpu()) 并合理分片任务、避免阻塞,才能实现真正多核并行。

goroutine 本身不等于并行计算;不显式配置调度器、不控制并发粒度、不避免阻塞,你写的只是并发,不是并行。
为什么 goroutine 默认不并行
Go 运行时默认只绑定 1 个 OS 线程(GOMAXPROCS=1),所有 goroutine 在单个 P(Processor)上协作调度——哪怕启了 1000 个 go f(),CPU 使用率也卡在 100%(单核)。这不是 bug,是设计:goroutine 是并发单元,不是并行单元。
- 可通过
runtime.GOMAXPROCS(0)查当前值,验证是否被容器/CI 环境重置为 1 - Go 1.5+ 虽默认设为
runtime.NumCPU(),但嵌入式、Docker、某些 CI 镜像仍可能保留旧行为 -
pprof中若高频出现runtime.mcall或runtime.gopark,说明 goroutine 频繁让出 P,未真正并行
设置 GOMAXPROCS 才能触发多核调度
必须在程序启动早期(如 main() 第一行)调用 runtime.GOMAXPROCS(runtime.NumCPU()),否则后续 goroutine 仍跑在单线程上。
- 不要依赖环境变量
GOMAXPROCS:它可能被父进程覆盖,且无法在运行时动态生效 - 设为
0是读取,不是设置;设为负数会 panic - 超核数设置(如 16 核设 32)通常无收益,反而增加调度开销;CPU 密集任务建议严格匹配物理核数
CPU 密集型任务必须分片 + 固定 worker 数
盲目启动大量 goroutine 做纯计算,只会导致栈内存暴涨、调度延迟、缓存失效,性能反而下降。
- 用
for i := range chunks切数据,每个 chunk 交给一个 goroutine 处理,worker 数控制在runtime.NumCPU()级别 - 禁用
time.Sleep、fmt.Println等非必要系统调用——它们会触发 goroutine 阻塞并让出 P - 结果收集优先走
channel(带缓冲,容量 = worker 数),而非全局变量 +sync.Mutex - 示例关键片段:
runtime.GOMAXPROCS(runtime.NumCPU()) ch := make(chan int, runtime.NumCPU()) for _, chunk := range split(data, runtime.NumCPU()) { go func(c []int) { ch
channel 同步比锁更可控,但要注意缓冲与关闭
用 sync.WaitGroup 等 goroutine 结束再读 channel,容易死锁;用 close(ch) + for range ch 更安全,但必须确保所有 sender 完成后才 close。
- 无缓冲 channel 在 sender 和 receiver 都就绪时才通行,适合精确同步;但用于结果收集易阻塞
- 缓冲 channel(容量 ≥ worker 数)可解耦发送与接收,避免 sender 卡住
- 多个 sender 时,必须由主 goroutine 统一 close,不能由某个 worker close
- receiver 侧用
for v := range ch自动退出,比for i := 0; i 更健壮(尤其当某 worker panic 时)
真正并行的难点不在启动 goroutine,而在控制调度边界、数据分片粒度和同步时机。一个没关的 channel、一次意外的系统调用、一个忘设的 GOMAXPROCS,都足以让并行退化为串行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











