为什么需要并发控制

Go 的 goroutine 很便宜,但便宜不等于可以无节制地起。真实业务里并发任务通常有三类约束:要等所有任务跑完(WaitGroup)、任务之间要共享错误并快速取消(errgroup)、还要限制同时跑的数量(semaphore)。这三个原语分别解决「等齐」「出错即停」「限量」三个不同的问题。

一、WaitGroup:等我的人都到齐

最朴素的同步原语。Add 登记任务数,每个 goroutine 退出前 Done,Wait 阻塞直到计数归零。

var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait() // 全部完成才继续

坑点:Add 必须在启动 goroutine 之前调用,否则可能在 Wait 已返回后才 Add,直接 panic。循环变量要用参数传入,别在闭包里直接捕获。

二、errgroup:出错即停,错误传递

WaitGroup 只能等齐,不能处理错误,也不能在其中一个任务失败时主动取消其他任务。errgroup 在 WaitGroup 之上加了 context 取消和错误传播能力。

g, ctx := errgroup.WithContext(context.Background())
for _, url := range urls {
    url := url
    g.Go(func() error {
        return fetchWithCtx(ctx, url) // 任一返回 error,ctx 即被取消
    })
}
if err := g.Wait(); err != nil {
    log.Fatal(err) // 拿到第一个非 nil 错误
}

关键点:g.Go 里启动的函数必须监听 ctx 并支持取消,否则「出错即停」只是空话。需要限制并发数时,配合 semaphore 使用(见下)。

三、semaphore:给并发装上节流阀

errgroup 默认不限并发,瞬间起几百个 goroutine 可能打垮下游。用 golang.org/x/sync/semaphore 的加权信号量,限制同时运行的任务数。

sem := semaphore.NewWeighted(int64(10)) // 最多 10 个并发
g, ctx := errgroup.WithContext(ctx)
for _, job := range jobs {
    if err := sem.Acquire(ctx, 1); err != nil {
        break
    }
    j := job
    g.Go(func() error {
        defer sem.Release(1)
        return do(j)
    })
}
g.Wait()

三者怎么选

原语解决的核心问题能处理错误吗
WaitGroup等所有任务跑完不能
errgroup等齐 + 出错即停 + 错误传递
semaphore限制并发数量(节流)需配合 errgroup

组合才是常态

生产代码里三者几乎总是组合使用:errgroup 管生命周期与错误,semaphore 管并发度,WaitGroup 作为底层机制被 errgroup 封装。记住一句原则——并发数量由 semaphore 决定,错误与取消由 errgroup 负责,别把三个职责混在一个原语里硬凑。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注