Go并发编程sync.WaitGroup | 青训营笔记

98 阅读4分钟

数据结构

为什么需要 WaitGroup

想象一个场景:我们有一个用户画像服务,当一个请求到来时,需要

  1. 从 request 里解析出 user_id 和 画像维度参数
  2. 根据 user_id 从 ABCDE 五个子服务(数据库服务、存储服务、rpc服务等)拉取不同维度的信息
  3. 将读取的信息进行整合,返回给调用方

假设 ABCDE 五个服务的响应时间 p99 是 20~50ms 之间。如果我们顺序调用 ABCDE 读取信息,不考虑数据整合消耗时间,服务端整体响应时间 p99 是:

sum(A, B, C, D, E) => [100ms, 250ms]

先不说业务上能不能接受,响应时间上显然有很大的优化空间。最直观的优化方向就是,取数逻辑的总时间消耗:

sum(A, B, C, D, E) -> max(A, B, C, D, E)

具体到 coding 上,我们需要并行调用 ABCDE 五个子服务,待调用全部返回以后,进行数据整合。如何保障全部返回呢?

此时,sync.WaitGroup 闪耀登场。


sync.WaitGroup

sync.WaitGroup是为了解决任务编排而出现的, 主要就是解决并发-等待问题, 因此在真正编写过程中也很常用。

WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量,每一次执行Add都会增加线程组的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。

如果你有多个goroutine并发执行某项操作,但是下方的代码需要等待这些并发的操作执行完成之后才能够执行,那么你可以使用sync.WaitGroup,因为它比Sleep更加有效,并且稳定。

WaitGroup 位于golang sync 包下,对应的类声明中包含了几个核心字段:

  • noCopy:这是防拷贝标识,标记了 WaitGroup 不应该用于值传递
  • state1:这是 WaitGroup 的核心字段,是一个无符号的64位整数,高32位是 WaitGroup 中并发计数器的数值,即当前 WaitGroup.AddWaitGroup.Done 之间的差值;低 32 位标识了,当前有多少 goroutineWaitGroup.Wait操作而处于阻塞态,陷入阻塞态的原因是因为计数器的值没有清零,即 state1 字段高 32 位是一个正值
  • state2:用于阻塞和唤醒 goroutine 的信号量

image.png

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

下面看一个简单使用的例子

package main
import (
    "fmt"
    "sync"
    "time"
)
var wg sync.WaitGroup
 
func foo(){
    defer wg.Done()
    fmt.Println("foo")
    time.Sleep(time.Second*2)
    fmt.Println("foo end")
}
 
func bar(){
    defer wg.Done() 
    fmt.Println("bar")
    time.Sleep(time.Second*2)
    fmt.Println("bar end")
}
 
func main(){
    start:=time.Now()
    wg.Add(2) //括号里面的值为执行并发程序的个数
    go foo()
    go bar()
    wg.Wait() //通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
    fmt.Println("程序结束,运行时间为",time.Now().Sub(start))
}

程序比较简单。

可以看一下下面的表格。

方法名功能
(wg * WaitGroup) Add(delta int)计数器+delta
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到计数器变为0

Add

Add主要是对state1字段中的计数值部分进行操作, 步骤如下:

  1. 将参数delta左移32位
  2. 将值加到计数值上(state1[1]), 这个值是可负可正

Done

Done实际上就是调用Add(-1)

而检测到-1之后的计数值为0时, 通过信号量唤醒正在wait的阻塞协程

Wait

调用wait时, 会将waiter+1, 然后将自身加入到等待队列中并阻塞, 等待Done时的唤醒

总结

这里暂时列举大部分,先留个坑,后期会把源码分析的内容也放到这个博客上面来分享一下。

参考

zhuanlan.zhihu.com/p/632190680

www.cnblogs.com/chnmig/p/16…

studygolang.com/articles/11…

你真的会用sync.WaitGroup吗?zhuanlan.zhihu.com/p/75441551

【golang】sync.WaitGroup详解