关于golang:最清晰易懂的-Go-WaitGroup-源码剖析

2次阅读

共计 3007 个字符,预计需要花费 8 分钟才能阅读完成。

hi,大家好,我是 haohongfan。

本篇次要介绍 WaitGroup 的一些个性,让咱们从实质下来理解 WaitGroup。对于 WaitGroup 的根本用法这里就不做过多介绍了。绝对于《这可能是最容易了解的 Go Mutex 源码分析》来说,WaitGroup 就简略的太多了。

源码分析

Add()

Wait()

type WaitGroup struct {
    noCopy noCopy
    state1 [3]uint32
}

WaitGroup 底层构造看起来简略,但 WaitGroup.state1 其实代表三个字段:counter,waiter,sema。

  • counter:能够了解为一个计数器,计算通过 wg.Add(N), wg.Done() 后的值。
  • waiter:以后期待 WaitGroup 工作完结的期待者数量。其实就是调用 wg.Wait() 的次数,所以通常这个值是 1。
  • sema:信号量,用来唤醒 Wait() 函数。

为什么要将 counter 和 waiter 放在一起?

其实是为了保障 WaitGroup 状态的完整性。举个例子,看上面的一段源码

// sync/waitgroup.go:L79 --> Add()
if v > 0 || w == 0 { // v => counter, w => waiter
    return
}
// ...
*statep = 0
for ; w != 0; w-- {runtime_Semrelease(semap, false, 0)
}

当同时发现 wg.counter <= 0 && wg.waiter != 0 时,才会去唤醒期待的 waiters,让期待的协程持续运行。然而应用 WaitGroup 的调用方个别都是并发操作,如果不同时获取的 counter 和 waiter 的话,就会造成获取到的 counter 和 waiter 可能不匹配,造成程序 deadlock 或者程序提前结束期待。

如何获取 counter 和 waiter ?

对于 wg.state 的状态变更,WaitGroup 的 Add(),Wait() 是应用 atomic 来做原子计算的 (为了防止锁竞争)。然而因为 atomic 须要使用者保障其 64 位对齐,所以将 counter 和 waiter 都设置成 uint32,同时作为一个变量,即满足了 atomic 的要求,同时也保障了获取 waiter 和 counter 的状态完整性。但这也就导致了 32 位,64 位机器上获取 state 的形式并不相同。如下图:

简略解释下:

因为 64 位机器上自身就能保障 64 位对齐,所以依照 64 位对齐来取数据,拿到 state1[0], state1[1] 自身就是 64 位对齐的。然而 32 位机器上并不能保障 64 位对齐,因为 32 位机器是 4 字节对齐,如果也依照 64 位机器取 state[0],state[1] 就有可能会造成 atmoic 的应用谬误。

于是 32 位机器上空出第一个 32 位,也就使前面 64 位人造满足 64 位对齐,第一个 32 位放入 sema 刚好适合。晚期 WaitGroup 的实现 sema 是和 state1 离开的,也就造成了应用 WaitGroup 就会造成 4 个字节节约,不过 go1.11 之后就是当初的构造了。

为什么流程图里短少了 Done ?

其实并不是,是因为 Done 的实现就是 Add. 只不过咱们惯例用法 wg.Add(1) 是加 1,wg.Done() 是减 1,即 wg.Done() 能够用 wg.Add(-1) 来代替。只管咱们晓得 wg.Add 能够传递正数当 wg.Done 应用,然而还是别这么用。

退出 waitgroup 的条件

其实就一个条件,WaitGroup.counter 等于 0

日常开发中非凡需要

1. 管制超时 / 谬误管制

虽说 WaitGroup 可能让主 Goroutine 期待子 Goroutine 退出,然而 WaitGroup 遇到一些非凡的需要,如:超时,谬误管制,并不能很好的满足,须要做一些非凡的解决。

用户在电商平台中购买某个货物,为了计算用户能优惠的金额,须要去获取 A 零碎(权利零碎),B 零碎(角色零碎),C 零碎(商品零碎),D 零碎(xx 零碎)。为了进步程序性能,可能会同时发动多个 Goroutine 去拜访这些零碎,必然会应用 WaitGroup 期待数据的返回,然而存在一些问题:

  1. 当某个零碎产生谬误,期待的 Goroutine 如何感知这些谬误?
  2. 当某个零碎响应过慢,期待的 Goroutine 如何管制拜访超时?

这些问题都是间接应用 WaitGroup 没法解决的。如果间接应用 channel 配合 WaitGroup 来管制超时和谬误返回的话,封装起来并不简略,而且还容易出错。咱们能够采纳 ErrGroup 来代替 WaitGroup。

无关 ErrGroup 的用法这里就不再论述。golang.org/x/sync/errgroup

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "time"
)

func main() {ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    errGroup, newCtx := errgroup.WithContext(ctx)

    done := make(chan struct{})
    go func() {
        for i := 0; i < 10; i++ {errGroup.Go(func() error {time.Sleep(time.Second * 10)
                return nil
            })
        }
        if err := errGroup.Wait(); err != nil {fmt.Printf("do err:%v\n", err)
            return
        }
        done <- struct{}{}
    }()

    select {case <-newCtx.Done():
        fmt.Printf("err:%v", newCtx.Err())
        return
    case <-done:
    }
    fmt.Println("success")
}

2. 管制 Goroutine 数量

场景模仿:
大略有 2000 – 3000 万个数据须要解决,依据对服务器的测试,当启动 200 个 Goroutine 解决时性能最佳。如何管制?

遇到诸如此类的问题时,单纯应用 WaitGroup 是不行的。既要保障所有的数据都能被解决,同时也要保障同时最多只有 200 个 Goroutine。这种问题须要 WaitGroup 配合 Channel 一块应用。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {var wg = sync.WaitGroup{}
    manyDataList := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    ch := make(chan bool, 3)
    for _, v := range manyDataList {wg.Add(1)
        go func(data int) {defer wg.Done()

            ch <- true
            fmt.Printf("go func: %d, time: %d\n", data, time.Now().Unix())
            time.Sleep(time.Second)
            <-ch
        }(v)
    }
    wg.Wait()}

应用留神点

应用 WaitGroup 同样不能被复制。具体例子就不再剖析了。具体分析过程能够参见《这可能是最容易了解的 Go Mutex 源码分析》

WaitGroup 的分析到这里根本就完结了。有什么想跟我交换的,欢送评论区留言。

欢送关注我的公众号:HHFCodeRV,一起学习一起提高

正文完
 0