根本原因是任务分发与worker退出的同步未对齐;需独立控制jobs关闭、workers运行和results接收三个信号,jobs channel宜用非缓冲或小缓冲,worker须用for job := range jobs确保不漏任务。

为什么用 chan 实现 Worker Pool 容易卡死或漏任务
根本原因不是 channel 本身,而是任务分发和 worker 退出的同步没对齐。常见现象是:所有 job 发完了,但部分 result 没收全;或者 worker 提前退出,导致后续 job 被丢弃。
关键在于三个信号必须独立控制:jobs 输入流是否关闭、workers 是否还在消费、results 是否收完。不能靠 close(jobs) 就认为全部结束。
-
jobschannel 应该是unbuffered或小缓冲(比如make(chan Job, 10)),避免发送端阻塞太久或积压失控 - 每个
worker必须用for job := range jobs,而不是for { select { case job := —— 后者在 <code>jobs关闭后会 panic - 结果收集端不能只等
len(results) == len(jobs),因为并发下长度不可靠;要用sync.WaitGroup或resultchannel 配合close()通知
workerPool.Run() 里要不要用 sync.WaitGroup
要,而且必须放在启动 worker goroutine 之前 Add(),在 worker 函数末尾 Done()。否则会出现 race:main 协程提前退出,worker 还在跑,结果丢失。
典型错误是把 wg.Add(1) 放在 go func() { ... }() 里面,导致 Add 和 Done 不在同一线程,Wait() 永远不返回。
立即学习“go语言免费学习笔记(深入)”;
- 正确姿势:
wg.Add(numWorkers)在启动循环外,每个go worker(...)里最后调用wg.Done() - 如果用
context.Context控制超时,wg.Wait()应该包在select里,避免 main 协程无限等待 - 不要用
time.Sleep()替代wg.Wait()—— 看似能跑通,但实际依赖运气,压力大时必出错
如何安全地停止正在运行的 Worker Pool
直接 close(jobs) 只能阻止新任务进入,已取出但未处理的 job 还在 worker 手里。真正安全停止需要两阶段:先拒绝新任务,再等正在执行的任务完成。
推荐用 context.WithCancel 配合每个 worker 内部检查 ctx.Done(),但注意:不能只靠 ctx 中断正在执行的 CPU 密集型逻辑,得配合可中断的计算结构(如带 select 的循环)。
- 每个
worker的主循环应是:for { select { case -
jobschannel 本身不用关,ctx取消后所有worker自行退出,wg.Wait()自然收尾 - 如果任务本身支持中断(比如 HTTP 请求带
ctx),就直接透传;否则需在任务内部定期检查ctx.Err()
Buffered channel 大小设成多少才不拖慢性能
没有通用值,取决于任务平均耗时和吞吐波动。设太大(比如 10000)会让内存占用虚高,且掩盖背压问题;设太小(比如 1)会让发送端频繁阻塞,降低吞吐。
真实场景中,buffer 大小本质是在「平滑突发流量」和「及时反馈过载」之间权衡。观察点不是 CPU 或内存,而是 len(jobs) 在高峰期是否长期接近 buffer cap。
- 初始建议从
runtime.NumCPU()的 2–4 倍起步(例如 8~16),再根据监控调整 - 如果任务耗时稳定(如固定 DB 查询),buffer = worker 数量即可
- 绝对不要用
make(chan Job, math.MaxInt)或类似 magic number —— 这等于放弃背压,OOM 风险极高
最常被忽略的一点:worker pool 的核心不是并发数量,而是 channel 的关闭时机和结果收集的完成判定。很多 bug 表面看是 goroutine 泄漏,实际是 result channel 没 close,或者 wg 没等全。










