在Go语言中,sync.WaitGroup 是一种用于等待一组 goroutine 完成的同步机制。正确使用 sync.WaitGroup 可以避免并发编程中的一些常见陷阱。以下是一些关键点和最佳实践:
WaitGroup在使用 WaitGroup 之前,必须先初始化它。通常使用 var 声明一个 WaitGroup 变量,然后直接使用它。
var wg sync.WaitGroup
Add 方法在启动每个 goroutine 之前,调用 wg.Add(1) 来增加 WaitGroup 的计数器。这个计数器表示有多少个 goroutine 需要等待。
wg.Add(1)
go func() {
defer wg.Done()
// 你的代码
}()
Done 方法在每个 goroutine 完成时,调用 wg.Done() 来减少 WaitGroup 的计数器。通常使用 defer 来确保 Done 一定会被调用,即使在 goroutine 中发生了 panic。
go func() {
defer wg.Done()
// 你的代码
}()
Wait 方法在主 goroutine 中调用 wg.Wait() 来阻塞,直到所有 goroutine 都完成(即 WaitGroup 的计数器变为 0)。
wg.Wait()
确保 Add 和 Done 的调用次数匹配,否则会导致 WaitGroup 的计数器变为负数,从而引发 panic。
Add不要在 goroutine 内部调用 wg.Add(1),因为这样可能会导致主 goroutine 在调用 Wait 时,WaitGroup 的计数器还没有被正确增加。
WaitGroup 的典型模式以下是一个典型的使用 WaitGroup 的模式:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
Wait 之后修改 WaitGroup一旦调用了 Wait,WaitGroup 的计数器应该已经变为 0,此时不应该再对 WaitGroup 进行任何操作。
WaitGroup 与 context 结合在某些情况下,你可能需要结合 context 来管理 goroutine 的生命周期,以便在超时或取消时优雅地停止 goroutine。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-time.After(time.Second):
fmt.Printf("Worker %d done\n", id)
case <-ctx.Done():
fmt.Printf("Worker %d canceled\n", id)
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
正确使用 sync.WaitGroup 的关键在于确保 Add 和 Done 的调用次数匹配,并且在适当的地方调用 Wait。遵循这些最佳实践可以避免并发编程中的常见陷阱,确保程序的正确性和稳定性。