登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

并发计数复用

来源:17golang原创

时间:2026-11-11 16:30:11 355浏览 收藏

先说结论:sync.WaitGroup 可以复用,但复用不是把计数器“清零”后马上继续写。上一轮任务必须已经完成,上一轮的所有 Wait 调用也必须已经返回,之后才能为下一组独立任务重新调用正向 Add。

判断一个复用写法是否安全,可以只看三件事:本轮是否在启动任务前建立了计数、本轮每个任务是否恰好调用一次 Done、下一轮的 Add 是否发生在上一轮所有 Wait 返回之后。下面按这个边界拆开说明。

先把一轮任务的计数归零

WaitGroup 维护的是当前这一轮仍未完成的任务数。正向 Add 建立计数,任务结束时用 Done 归还一个计数,Wait 则等待计数回到零。计数为零时,从零开始的正向 Add 必须先于对应的 Wait。

package main

import "sync"

func runBatch(tasks []func()) {
	var wg sync.WaitGroup

	// 先建立本轮任务总数,再启动 goroutine,避免 Wait 先看到零。
	wg.Add(len(tasks))
	for _, task := range tasks {
		current := task
		go func() {
			defer wg.Done() // 每个任务结束时只归还一次计数。
			current()
		}()
	}

	// 当前轮计数归零后,Wait 才会返回。
	wg.Wait()
}
WaitGroup 中任务集合、Add、goroutine、Done 和 Wait 的静态关系说明图
图1:结构说明图,查看当前轮任务集合与 Add、Done、Wait 的静态关系;这不是运行截图。

这里的顺序很重要。把 Add 放进新启动的 goroutine,会让 Add 与 Wait 竞争:如果 Wait 先观察到计数为零,它可能提前返回,外层代码就会误以为本轮已经结束。即使偶尔运行正确,也不能把这种时序交给调度器碰运气。

复用的真正边界是所有 Wait 都返回

假设程序要连续处理多组独立任务,安全的复用形态是“上一轮完全收口,下一轮才建账”。官方文档的限制不是“看到计数为零就立即 Add”,而是新一轮的正向 Add 必须发生在所有上一轮 Wait 调用都返回之后。

func runRounds(rounds [][]func()) {
	var wg sync.WaitGroup

	for _, tasks := range rounds {
		// 当前轮从零开始计数;此处位于上一轮 Wait 返回之后。
		wg.Add(len(tasks))
		for _, task := range tasks {
			current := task
			go func() {
				defer wg.Done() // 当前任务结束后归还本轮计数。
				current()
			}()
		}
		wg.Wait() // 只有本轮收口后,循环才进入下一轮。
	}
}
WaitGroup 多轮任务中上一轮 Wait 返回与下一轮 Add 的边界说明图
图2:边界说明图,突出上一轮 Wait 返回与下一轮 Add 之间的复用条件;这不是执行时序截图。

如果有多个 goroutine 同时调用同一个 WaitGroup.Wait,也要把它们都视为上一轮的一部分:不能只等其中一个返回,就让下一轮从零开始 Add。需要跨轮并行时,应为不同的独立任务集合使用不同的同步边界,而不是让同一个计数器同时承担两轮生命周期。

三个容易误判的写法

  • 在 goroutine 内 Add:启动 goroutine 后才增加计数,可能与外层 Wait 竞争。应在创建 goroutine 前完成正向 Add。
  • 计数刚归零就复用:如果还有上一轮的 Wait 调用没有返回,不能开始下一轮。判断标准是所有 Wait 返回,而不是某个任务刚好结束。
  • 复制已经使用过的 WaitGroup:WaitGroup 首次使用后不能复制。把它作为值传递、放进会复制的结构体,都会让同步状态的归属变得不明确。

另外,Done 等价于 Add(-1),多调用一次会使计数变成负数并触发 panic。因此把 Done 放到 goroutine 开头、分支遗漏 Done,或在 defer 和显式代码中重复归还,都会破坏这一轮的计数闭环。

用一张表快速判断能否复用

检查点安全条件不满足时的风险
本轮 Add计数为零时,先 Add 再启动任务和 WaitWait 可能提前返回
本轮 Done每个任务恰好一次,通常用 defer 兜底永久等待或负数 panic
跨轮复用所有上一轮 Wait 都已返回两轮生命周期重叠,计数归属不清
对象传递首次使用后不复制 WaitGroup不同副本各自维护状态,等待结果失真

相关问题

WaitGroup 能不能在循环里重复使用?

可以,前提是每一轮都在上一轮所有 Wait 返回后才重新 Add,并且每轮任务都完整调用 Done。

为什么不能只看计数是否回到零?

因为复用条件还涉及正在进行的 Wait 调用。文档要求所有上一轮 Wait 返回,单看计数变化不足以表达完整的跨轮边界。

把 WaitGroup 当作“一轮任务的计数边界”而不是可随意重置的全局变量,复用问题就会简单很多:先建立当前轮,再等待当前轮收口,最后才允许下一轮开始。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>