Go语言如何实现任务并行执行_Golang并发任务拆分

5次阅读

应控制goroutine并发数,用channel信号量或worker pool限流;闭包需传参防变量共享;错误管理优先用errgroup.Group统一处理。

Go语言如何实现任务并行执行_Golang并发任务拆分

goroutine 启动并行任务,但别直接裸奔

Go 里最常用的并行方式就是起 goroutine,但它不是“开越多越快”的银弹。比如批量调用 http 接口、处理一批文件、或并发查询数据库,你写 go doTask(item) 很容易,但没控制数量就可能打爆内存或触发限流。

实际做法是配合 sync.WaitGroup 等待全部完成,并用带缓冲的 channel 或 worker pool 控制并发数:

var wg sync.WaitGroup sem := make(chan struct{}, 10) // 最多 10 个并发 

for _, item := range tasks { wg.Add(1) go func(t Task) { defer wg.Done() sem <- struct{}{}>

  • 不加限制时,goroutine 数量可能瞬间上千,GC 压力陡增
  • sem 这种 channel 信号量比 sync.Mutex 更轻量,适合纯计数场景
  • 注意闭包捕获变量:必须把 item 作为参数传入匿名函数,否则所有 goroutine 可能共享最后一个值

errgroup.Group 统一管理错误和生命周期

原生 sync.WaitGroup 不支持错误传播,一旦某个任务出错,其他还在跑,你得自己设标志位或加锁判断。而 errgroup.Group(来自 golang.org/x/sync/errgroup)天然支持“任一出错即取消其余”。

适用场景:需要强一致性失败语义的任务链,比如批量写入 ES、同步多个下游服务:

立即学习go语言免费学习笔记(深入)”;

g, ctx := errgroup.WithContext(context.Background()) for _, url := range urls {     url := url // 防止闭包问题     g.Go(func() error {         resp, err := http.Get(url)         if err != nil {             return err         }         defer resp.Body.Close()         return nil     }) } if err := g.Wait(); err != nil {     log.Printf("at least one request failed: %v", err) }
  • WithContext 返回的 ctx 会在任意子任务返回错误时被 cancel,可用于提前中断长耗时操作
  • 它内部用了 sync.WaitGroup + sync.Once线程安全,无需额外加锁
  • 如果任务本身不接受 context.Context,记得在函数体内用 select { case 主动响应取消

拆分任务时优先按数据边界而非固定数量切片

很多人习惯把一个大 slice 按每 100 个元素切一块,然后并发处理。但这在真实场景中常导致负载不均——比如某些分块里全是大文件、慢接口或重计算项,结果大部分 goroutine 早结束了,只剩一两个卡着。

更稳妥的做法是按“工作单元”本身拆分,而不是数组下标:

  • 对文件处理:按文件粒度并发,而不是按字节范围切分(除非你在做 mmap 或流式解析)
  • 对数据库查询:用 IN 批量查不如按主键范围分页(如 id BETWEEN ? AND ?),避免单条 SQL 过长或命中率低
  • 对 API 调用:若后端支持批量接口(如 /batch?ids=a,b,c),优先用批量,而不是把单 ID 请求并发化

简单粗暴的等长切片只适合各单元耗时方差极小的场景,比如纯内存 JSON 解析。

runtime.GOMAXPROCS 一般不用手动调,但要注意 CGO 场景

默认情况下,Go 运行时会把 GOMAXPROCS 设为 CPU 核心数,这对绝大多数纯 Go 并发任务已足够。强行设高不会提升吞吐,反而增加调度开销。

唯一需要关注它的场景是启用了 CGO 且调用大量阻塞式 C 函数(如某些加密库、旧版 SQLite 驱动):

  • 每个阻塞的 CGO 调用会占用一个 OS 线程,且该线程无法被 Go 调度器复用
  • 此时若 GOMAXPROCS 太小,新 goroutine 可能因无可用 P 而饿死
  • 解决方案不是狂拉 GOMAXPROCS,而是改用非阻塞替代方案,或用 runtime.LockOSThread() + 单独线程池隔离 CGO

大多数现代 Go 项目根本碰不到这个问题,但一旦遇到“并发数上不去、CPU 却很低”的诡异现象,就得回头查是不是 CGO 在暗处拖后腿。

text=ZqhQzanResources