waitGroup 使用方法

比如,我们要完成一个大的任务,需要使用并行的 goroutine 执行三个小任务,只有这三 个小任务都完成,我们才能去执行后面的任务。如果通过轮询的方式定时询问三个小任务

是否完成,会存在两个问题:一是,性能比较低,因为三个小任务可能早就完成了,却要 等很长时间才被轮询到;二是,会有很多无谓的轮询,空耗 CPU 资源。

那么,这个时候使用 WaitGroup 并发原语就比较有效了,它可以阻塞等待的 goroutine。等到三个小任务都完成了,再即时唤醒它们。 其实,很多操作系统和编程语言都提供了类似的并发原语。比如,Linux 中的 barrier、 Pthread(POSIX 线程)中的 barrier、C++ 中的 std::barrier、Java 中的 CyclicBarrier 和 CountDownLatch 等。由此可见,这个并发原语还是一个非常基础的并发类型。所 以,我们要认真掌握今天的内容,这样就可以举一反三,轻松应对其他场景下的需求了。

WaitGroup 的基本用法 Go 标准库中的 WaitGroup 提供了三个方法,保持了 Go 简洁的风格。

1
2
3
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

Add,用来设置 WaitGroup 的计数值; Done,用来将 WaitGroup 的计数值减 1,其实就是调用了 Add(-1); Wait,调用这个方法的 goroutine 会一直阻塞,直到 WaitGroup 的计数值变为 0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 线程安全的计数器
type Counter struct {
    mu sync.Mutex
    count uint64
}
// 对计数值加一
func (c *Counter) Incr() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}
// 获取当前的计数值
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}
// sleep 1秒,然后计数值加1
func worker(c *Counter, wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(time.Second)
    c.Incr()
}
func main() {
    var counter Counter
    var wg sync.WaitGroup
    wg.Add(10) // WaitGroup的值设置为10
    for i := 0; i < 10; i++ { // 启动10个goroutine执行加1任务
    	go worker(&counter, &wg)
    }
// 检查点,等待goroutine都完成任务
	wg.Wait()
// 输出当前计数器的值
	fmt.Println(counter.Count())
}