在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
。遵循这些最佳实践可以避免并发编程中的常见陷阱,确保程序的正确性和稳定性。