Go语言线程池:如何在Goroutine等待时启动新协程?
问题本质
你当前用errgroup.SetLimit(10)实现的是严格限制同时处于活跃状态(未完成)的goroutine总数——哪怕这10个goroutine全卡在IO等待中,新的goroutine也不会启动,白白浪费了CPU资源。你需要的是:当某个goroutine因IO阻塞(暂时让出CPU)时,就允许启动新的goroutine,直到CPU被充分利用。
解决方案
1. 针对IO密集型任务:用GOMAXPROCS倍数控制并发
Go的调度器本身会在goroutine阻塞时自动调度其他goroutine,你可以放弃errgroup.SetLimit,改用基于CPU核心数的信号量来控制并发,既避免goroutine数量爆炸,又能利用IO等待时的CPU空闲:
import ( "runtime" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group // 信号量大小设为GOMAXPROCS的4倍(可根据IO密集程度调整,比如2-10倍) sem := make(chan struct{}, runtime.GOMAXPROCS(0)*4) for i := 0; i < 1000000; i++ { i := i sem <- struct{}{} // 获取信号量,控制并发启动数 g.Go(func() error { defer func() { <-sem }() // 任务完成后释放信号量 task(i) // 包含IO等待的长耗时任务 return nil }) } g.Wait() }
2. 拆分CPU与IO逻辑(精准控制,推荐)
如果能修改task函数,把CPU密集和IO密集的部分分开,在CPU执行前后获取/释放信号量,这样当goroutine进入IO等待时,信号量会被释放,允许新的goroutine执行CPU任务:
import ( "runtime" "golang.org/x/sync/errgroup" ) // 全局信号量,限制同时执行CPU任务的goroutine数(设为GOMAXPROCS的2倍) var cpuSem = make(chan struct{}, runtime.GOMAXPROCS(0)*2) func task(i int) { // CPU密集操作:先获取信号量 cpuSem <- struct{}{} processData(i) // 比如解析、计算等CPU操作 <-cpuSem // 释放信号量,让出CPU名额 // IO密集操作:无限制,goroutine在此阻塞时会自动让出CPU writeToDatabase(i) // 比如DB写入、HTTP请求等IO操作 // 后续CPU操作,再次获取信号量 cpuSem <- struct{}{} finalizeResult(i) <-cpuSem } func main() { var g errgroup.Group for i := 0; i < 1000000; i++ { i := i g.Go(func() error { task(i) return nil }) } g.Wait() }
为什么errgroup.SetLimit无法满足需求?
errgroup.SetLimit的实现逻辑是:内部维护一个带缓冲的通道,每次调用Go()时先向通道发送元素,通道满则阻塞;当goroutine完成时,从通道接收元素。它限制的是未完成的goroutine总数,不管这些goroutine是在运行还是阻塞——这就是为什么即使有goroutine卡在IO等待,新的goroutine也无法启动。
内容的提问来源于stack exchange,提问作者harnoor
相关产品推荐
相关产品推荐

