应采用worker pool限制并发,用带缓冲channel或golang.org/x/sync/semaphore分发任务,避免循环中直接go f();结合pprof和trace定位调度瓶颈,调优gomaxprocs与gc参数,并杜绝长阻塞系统调用和锁竞争。

goroutine 创建太多导致调度延迟高怎么办
协程数量爆炸不是调度器坏了,而是它被迫扫描、迁移、GC 的节奏被打乱了。几十万 goroutine 同时存活,runtime.NumGoroutine() 持续 >10k,基本就是信号。
- 别在循环里直接写
go f()—— 尤其是处理 HTTP 请求、文件行、数据库结果集这种批量输入时 - 改用 worker pool:固定启动
runtime.NumCPU()或略高的 goroutine(如 8–16 个),通过带缓冲的chan Task分发任务 - 用
golang.org/x/sync/semaphore替代手写 channel 令牌,语义更清晰,支持带 context 的 acquire - 对短生命周期任务(如日志上报、metric 打点),优先走异步 buffer + 定期 flush,而不是每个动作启一个 goroutine
为什么设置了 GOMAXPROCS 还是跑不满 CPU
默认值通常已合理,盲目调高反而可能因上下文切换增多而拖慢吞吐。真正卡住并行度的,往往是隐式串行点。
- 检查是否所有 goroutine 都在争抢同一个无缓冲
chan—— 这会让它们排队执行,本质是单线程 - 确认有没有长阻塞系统调用:比如未设
ReadDeadline的net.Conn.Read,或没用os.ReadFile而是自己开*os.File+ReadAll - 看锁竞争:用
pprof -mutex抓热点,sync.Mutex持有时间超过 100μs 就该拆分或换sync.Map/atomic - I/O 密集型服务可试
GOMAXPROCS=2*runtime.NumCPU(),但必须配合非阻塞 I/O 使用,否则只是多几个等在 syscalls 上的线程
runtime.Gosched() 到底该不该用
它不是“让调度器更勤快”的开关,而是给长计算 loop 主动让出 M 的最后手段 —— 大多数情况不该出现,出现了说明设计有问题。
- 仅在纯计算、无函数调用、无 channel 操作的 tight loop 中考虑:比如图像像素遍历、密码学哈希内部循环
- 每 N 次迭代插一次
runtime.Gosched()(N 取 100–1000),避免单个 goroutine 独占 M 超过 10ms - 更优解是把大任务拆成小块,用 channel 或 callback 交还控制权,而不是靠 Gosched 补救
- 绝对不要在 hot path 的普通循环里滥用,它本身有开销,且掩盖了本该异步化或分片的真实问题
pprof 和 trace 怎么抓调度瓶颈
光看 CPU 占用率没用,得定位到“谁在等、等什么、等多久”。关键不是堆栈,而是调度事件流。
- 启动时加
GODEBUG=schedtrace=1000:每秒输出调度器状态,重点看idleP 数、runqueue长度突增、gc停顿是否频繁打断 M -
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2查看阻塞 goroutine 栈,找select卡死、chan send等待、sync.Mutex.Lock持有者 - 用
go tool trace打开 trace 文件后,聚焦 “Goroutines” 和 “Scheduler Latency” 视图:看 Goroutine 从 ready 到 running 的延迟是否稳定在 sub-ms 级别 - 如果 trace 里大量 goroutine 长时间处于
running状态却不 yield,说明有计算密集未分片;若大量卡在syscall,说明 I/O 没走 Go 封装的协作路径
go 外壳。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











