Go 的并发模型讲究 Goroutine,轻量、高效。程序员只要一个 go
关键字,便能启动新 Goroutine。可启动容易,等它们全部结束难。想象一下,多个工人在干活,工头得等所有工人干完才能下班。WaitGroup 就是这个工头。
WaitGroup,顾名思义,“等待一组”。背后有三主要动作:Add、Done、Wait。怎么理解?我给你举个小例子。
老王是包工头,他派了几个工人(Goroutine)搬砖。首先是安排任务。Add
就是安排任务数量。例如,今天有三车砖要搬,老王就告诉 WaitGroup:我们要搬三车,接着启动干活的人。
go
wg.Add(3)
这三个工人满怀干劲,各自奔向自己的砖车。而搬完之后,工人需要告诉老王,活儿干完了,Done 此时派上用场。每搬完一车砖,工人会喊一嗓子:我搞定了。这里不需要你传参数,Done 只做个减法,把欠的活儿数减一。
go
wg.Done() // 也可以写作 wg.Add(-1)
最后一个动作,Wait。你想想老王这个工头,派完了活儿,他就得等着。如果不等,工人还在搬砖,工头拍拍屁股走了,这批活就失控了。Wait 会阻塞老王的流程,阻止他提前收工。等所有工人干完活了,老王才能继续该干的事儿,比如结工钱、收工。
go
wg.Wait()
上面是 WaitGroup 基本的运用逻辑,底下呢? 哇,其实特别简单。
Go 的标准库中,WaitGroup 由三个核心字段支撑。counter、waiter 和 semaphore。别被专业词吓住,接着听。
- counter
:其实就是干活的量,你 Add 多少它记住,每 Done 一下它就减,到零了说明活儿完了。 - waiter
:像是等候室的计数器,Wait 一调用,表示“老王”进等候室了,直到 counter 归零,“老王”才能被放出来。 - semaphore
:不怎么起眼的小家伙,控制的是 Goroutine 之间信号的同步。譬如,有人在搬完砖后,会触发一个小信号通知“屋内”老王做检查。
你看这个简单的并发控制工具,调用时注意的是 race condition(竞争条件)。什么意思?假设多个工人(Goroutine)在修改同一个计数器,WaitGroup 帮你做了合适的同步。所有事都是并发安全的。程序员不用自己操心琐碎、复杂的多线程锁逻辑,少了很多负担。
特别提一点,使用 WaitGroup 要注意,别传值传递,要传指针。换句话说,WaitGroup 结构体传递必须谨慎,因为它背后有内在的状态管理。否则你等半天,工头没听见任何工人完成通知,直接走了,这也未必是好事。