跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

Golang怎么实现Goroutine池_Golang如何用固定大小的协程池执行并发任务【实战】

Go中goroutine泄漏比误用sync.Pool更常见,主因是任务卡住、panic未recover、channel关闭不当;应使用buffered channel+WaitGroup实现固定大小协程池,worker数需按IO/CPU密集型区分设置。 goroutine 泄漏比池子没用好更常见 Go 里没有内置的 goroutine 池,直接用
sync.Pool
管理 goroutine 是错的——它只适合复用对象,不负责调度或生命周期控制。真正要防的是:任务提交后 goroutine 卡住、panic 未 recover、channel 关闭不及时,导致 goroutine 永远挂起。 常见错误现象:
runtime.GOMAXPROCS(1)
下跑着跑着卡死;pprof 查
goroutine
数持续上涨;日志里反复出现
fatal error: all goroutines are asleep - deadlock!
。 永远在 worker goroutine 里加
recover()
,尤其处理用户传入的函数时 用带缓冲的
chan *task
(比如
make(chan *task, 100)
),避免 submit 阻塞导致调用方协程堆积 池关闭时,先关 input channel,再
waitGroup.Wait()
,最后关 result channel —— 顺序反了就漏 goroutine 用 buffered channel + waitGroup 实现最小可行池 不需要引入第三方库,15 行内能写出生产可用的固定大小池。核心是把“任务分发”和“worker 生命周期”拆开:channel 负责排队,
sync.WaitGroup
负责等待退出,
sync.Once
控制关闭幂等性。 使用场景:HTTP handler 中批量调第三方 API、日志异步刷盘、定时任务批量重试。 立即学习 “ go语言免费学习笔记(深入) ”; 示例关键结构:
type Pool struct { tasks chan *task workers int wg sync.WaitGroup once sync.Once } func (p *Pool) Submit(f func()) { p.tasks <- &task{fn: f} // 不阻塞,靠 buffer 抗峰 } func (p *Pool) startWorker() { defer p.wg.Done() for t := range p.tasks { // channel 关闭后自动退出 if t != nil && t.fn != nil { defer func() { recover() }() t.fn() } } }
worker 数设成 runtime.NumCPU() 多数时候是错的 CPU 密集型任务才该对齐 CPU 核数;IO 密集型(HTTP、DB、文件)反而需要更大池子,否则大量 goroutine 在 sysmon 里等网络就绪,实际并发度上不去。 参数差异:
runtime.GOMAXPROCS(n)
控制 P 的数量,影响调度器吞吐,和 worker 数无关 池大小建议从
50
起调,压测时看
go tool pprof
里 goroutine block 和 syscall wait 时间占比 超过 500 个 worker 且任务轻量(<1ms)时,channel 争用会成为瓶颈,考虑改用
sync.Map
分片任务队列 别让 context.WithTimeout 包裹整个 Submit 调用 常见错误是把超时套在
pool.Submit(...)
外层,结果任务进队列成功,但还没执行就超时返回——用户以为任务丢了,其实它还在池里排队。 正确做法:超时控制必须下沉到 task 内部,或者用带 deadline 的 channel select:
select { case p.tasks <- t: case <-time.After(2 * time.Second): return errors.New("pool full or busy") }
性能影响:每次 Submit 做 timeout select 有微小开销,但比丢任务或阻塞调用方强得多。如果池满是常态,说明任务处理不过来,该扩容或降级,而不是掩盖问题。 容易被忽略的是:worker 里执行的函数如果自己没接
context.Context
,timeout 就完全无效——池子只管调度,不管业务逻辑怎么跑。

相关文章