乐趣区

关于后端:Go语言-WaitGroup-源码知多少

后面的文章咱们写协程的时候有用到 WaitGroup

咱们的写法大略是这样的

func main() {...dothing()

    wg := sync.WaitGroup{}
    // 管制 多个子协程的申明周期
    wg.Add(xx)

    for i := 0; i < xx; i++ {go func(ctx context.Context) {defer wg.Done()
            ...dothing()}(ctx)
    }

    ...dothing()
    // 期待所有的子协程都优雅敞开
    wg.Wait()
    fmt.Println("close server")
}

能够看出,sync.WaitGroup 次要是用来期待一批协程敞开的,例如下面的 主协程 期待 所有子协程敞开,本人才进行退出

那么咱们明天就来探索一下 sync.WaitGroup 的源码实现吧

探索源码实现

sync.WaitGroup 的应用上述 dmeo 曾经给出,看上去用起来也很简略

应用 Add 函数是增加期待的协程数量

应用 Done 函数是告诉 WaitGroup 以后协程工作实现了

应用 Wait 函数 是期待所有的子协程敞开

咱关上源码

源码门路:src/sync/waitgroup.go,总共源码 141 行

单测文件 src/sync/waitgroup_test.go 301 行

源码文件总共 4 个函数,1 个构造体

  • type WaitGroup struct {
  • func (wg WaitGroup) state() (statep uint64, semap *uint32) {
  • func (wg *WaitGroup) Add(delta int) {
  • func (wg *WaitGroup) Done() {
  • func (wg *WaitGroup) Wait() {

咱们一一来瞅一瞅这几个函数都做了那些事件

type WaitGroup struct {

WaitGroup 期待一组 goroutine 实现,主 goroutine 调用 Add 来设置期待的 goroutines

而后是每一个协程调用 , 当实现时运行并调用 Done

与此同时,Wait 能够被用来阻塞,直到所有 goroutine 实现

WaitGroup 在第一次应用后不能被复制

咱们能够看到 WaitGroup 构造体有 2 个成员

  • noCopy

是 go 语言的源码中检测禁止拷贝的技术,如果检测到咱们的程序中 WaitGroup 有赋值的操作,那么程序就会报错

  • state1

能够看出 state1 是一个元素个数为 3 个数组,且每个元素都是 占 32 bits

64 位 零碎外面,64 位原子操作须要 64 位对齐

那么 高位的 32 bits 对应的是 counter 计数器,用来示意目前还没有实现工作的协程个数

低 32 bits 对应的是 waiter 的数量,示意目前曾经调用了 WaitGroup.Wait 的协程个数

那么剩下的一个 32 bits 就是 sema 信号量 的了(前面的源码中会有体现)

func (wg WaitGroup) state() (statep uint64, semap *uint32) {

持续看源码

// state returns pointers to the state and sema fields stored within wg.state1.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
    } else {return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
    }
}

此处咱们能够看到,state 函数是 返回存储在 wg.state1 中的状态和 sema 字段 的指针

这里须要重点留神 state() 函数的实现,有 2 种状况

  • 第 1 种 状况是,在 64 位零碎上面,返回 sema 字段 的指针取的是 &wg.state1[2],阐明 64 位零碎时,state1 数据排布是:counterwaitersema
  • 第 2 种状况是,32 位零碎上面,返回 sema 字段 的指针取的是 &wg.state1[0],阐明 64 位零碎时,state1 数据排布是:semacounterwaiter

具体起因仔细的 胖鱼 可能有点想法,

为什么在不同的操作系统外面,数据结构中的 state1 数组数据排布还不一样?

咱们认真看一下上述的源码

64 位零碎时:

return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]

32 位零碎时

return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]

golang 这样用,次要起因是 golang 把 counter 和 waiter 合并到一起对立看成是 1 个 64 位的数据了,因而在不同的操作系统中

因为字节对齐的起因,64 位零碎时,后面 2 个 32 位数据加起来,正好是 64 位,正好对齐

对于 32 位零碎,则是 第 1 个 32 位数据放 sema 更加适合,前面的 2 个 32 位数据就能够对立取出,作为一个 64 位变量

Add 函数 次要性能是将 counter +delta,减少期待协程的个数:

咱们能够看到 Add 函数,通过 state 函数获取到 上述 64 位的变量(counterwaiter)和 sema 信号 量后,通过 atomic.AddUint64 函数 将 delta 数据 加到 counter 下面

这里为什么是 delta 要左移 32 位呢?

下面咱们有说到嘛,state 函数拿出的 64 位变量,高 32 bits 是 counter,低 32 bits 是waiter,此处的 delta 是要加到 counter 上,因而才须要 delta 左移 32 位

func (wg *WaitGroup) Done() {

// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {wg.Add(-1)
}

Done 函数没有什么特地的,间接上调用 Add 函数来实现的

func (wg *WaitGroup) Wait() {

Wait 函数 次要是减少 waiter 的个数:

阻塞期待 WaitGroup 中 couter 的个数变成 0

函数次要是通过 atomic.CompareAndSwapUint64 函数 CAS(比拟并且替换)的形式来操作 waiter 的。

很显著该逻辑是 必须要是 true,能力走到外面的实现,进行 runtime_Semacquire(semap) 操作,若是 false,则须要在循环外面持续再来一次

Waitgroup .go 的具体实现尽管才 141 行 ,外面的具体细节咱们还须要重复深究,学习其中的设计原理,例如 state1 构造体成员的设计思维,就十分的奇妙,无需将它 拆成 3 个成员,进而无需再操作值的时候加锁,这样性能就得以很好的展示

缓缓的学习好的思维,日拱一卒

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

好了,本次就到这里

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

我是 阿兵云原生,欢送点赞关注珍藏,下次见~

退出移动版