• Golang sync.WaitGroup


    1. Golang sync.WaitGroup

    1.1. 基础知识

    这个是通过通道, 来控制 goroutine 协程结束的示例:

    func coordinateWithChan() { sign := make(chan struct{}, 2) num := int32(0) fmt.Printf("The number: %d [with chan struct{}]\n", num) max := int32(10) go addNum(&num, 1, max, func() {  sign <- struct{}{} }) go addNum(&num, 2, max, func() {  sign <- struct{}{} }) <-sign <-sign}
    
    • 1

    上一节我们学习过, sign 通道读取数据时, 如果命中"有缓冲 channel + 缓冲为空"的情况, 会阻塞, 只有两个 go 协程全部执行完毕, 往 sign 塞数据后, 程序才会退出, 但是这种方式非常繁琐。在这种应用场景下, 我们可以选用另外一个同步工具 sync.WaitGroup(以下简称 WaitGroup 类型), 它比通道更加适合实现这种一对多的 goroutine 协作流程。WaitGroup 类型是开箱即用的, 也是并发安全的, 它拥有三个指针方法: Add、Done 和 Wait, 你可以想象该类型中有一个计数器, 它的默认值是 0, 我们可以通过调用该类型值的 Add 方法来增加, 或者减少这个计数器的值, 代码升级如下:

    func coordinateWithWaitGroup() { var wg sync.WaitGroup wg.Add(2) // 计数器加 2 num := int32(0) fmt.Printf("The number: %d [with sync.WaitGroup]\n", num) max := int32(10) go addNum(&num, 3, max, wg.Done)  // 计数器减 1 go addNum(&num, 4, max, wg.Done)  // 计数器减 1 wg.Wait() // 会阻塞, 直到计数器值为 0, 然后就会被唤醒}
    
    • 1

    Add 会增加计数器的值, Done 会减少计数器的值, Wait 会一直阻塞, 直到计数器的值重新回归为 0, 然后才会被唤醒, 继续往后面执行。

    1.2. 常见的坑

    如果使用不当, 容易抛出 Panic, 我就把相关知识点列出来:

    • 坑 1(计数器为负数): sync.WaitGroup 类型值中计数器的值如果小于 0, 会直接抛出 Panic。
    • 坑 2(同时调用 Add 和 Wait): 如果我们对它的 Add 方法的首次调用, 与对它的 Wait 方法的调用是同时发起的, 比如, 在同时启用的两个 goroutine 中, 分别调用这两个方法, 那么就有可能会让这里的 Add 方法抛出一个 panic。
    • 坑 3(跨越计数周期): 如果在一个此类值的 Wait 方法被执行期间, 跨越了两个计数周期, 那么就会引发一个 panic。
      对于坑 1, 当调用 Add 方法, 传入一个负数的时候可能会出现, 所以我们使用 WaitGroup 时, 需要保证计数一直大于 0。对于坑 2, 需要说明一点, 虽然 WaitGroup 值本身并不需要初始化, 但是尽早地增加其计数器的值, 还是非常有必要的。对于坑 3, 我们需要先了解 WaitGroup 的计数周期:
      计数周期: WaitGroup 中计数器值由 0 变为了某个正整数, 而后又经过一系列的变化, 最终由某个正整数又变回了 0。也就是说, 只要计数器的值始于 0 又归为 0, 就可以被视为一个计数周期。在一个此类值的生命周期中, 它可以经历任意多个计数周期。但是, 只有在它走完当前的计数周期之后, 才能够开始下一个计数周期。那坑 3 什么情况会出现呢? 场景如下: 当前的 goroutine 因调用 Wait 方法被阻塞的时候, 另一个 goroutine 调用了该值的 Done 方法, 并使其计数器的值变为了 0, 这会唤醒当前的 goroutine, 并使它试图继续执行 Wait 方法中其余的代码。但在这时, 又有一个 goroutine 调用了它的 Add 方法, 并让其计数器的值又从 0 变为了某个正整数。此时, 这里的 Wait 方法就会立即抛出一个 panic。根据坑 2 和坑 3, 总结如下: 不要把增加其计数器值的操作和调用其 Wait 方法的代码, 放在不同的 goroutine 中执行。换句话说, 要杜绝对同一个 WaitGroup 值的两种操作的并发执行, 标准方式应该为"先统一 Add, 再并发 Done, 最后 Wait"。

    1.3. 并发实例: Push

    对于上一章的并发示例, 当时提了一个问题: 每消费一条 Channel 数据, 需要记录 Push 发送成功, 但是一条 Channel 数据包含 2-3 个 Push 内容 (IOS/Android/PC), 程序记录 Push 成功前, 如何保证这 2-3 个 Push 都发送完毕了呢? 根据"先统一 Add, 再并发 Done, 最后 Wait"原则, 看下面代码:

    var (   wg    sync.WaitGroup   succs []*NotifyMessage   fails []*NotifyMessage)for _, message := range t.PushMessages {   wg.Add(1)  // 计数加 1   go func(message mipush.PushMessage) {      defer func() {         wg.Done() // 计数减 1      }()      // 发送 IOS/Android/PC 等渠道的 Push      // 代码省略。..   }(message)}wg.Wait() // 阻塞, 直到计数器值为 0, 然后就会被唤醒// 数据统计 SendNotify(t.ID, t.TotalPage, t.TaskPage, t.AppType, t.AppLocal, fails, succs)
    
    • 1

    1.4. 总结

    WaitGroup 是开箱即用和并发安全的, 可以通过它很方便地实现一对多 goroutine 协作流程, 即: 一个分发子任务的 goroutine, 和多个执行子任务的 goroutine, 共同来完成一个较大的任务。在使用 WaitGroup 值的时候, 我们一定要注意, 千万不要让其中的计数器的值小于 0, 否则就会引发 panic。另外, 我们最好用"先统一 Add, 再并发 Done, 最后 Wait"这种标准方式, 来使用 WaitGroup 值, 尤其不要在调用 Wait 方法的同时, 并发地通过调用 Add 方法去增加其计数器的值, 因为这也有可能引发 panic。

  • 相关阅读:
    Mysql高可用
    精度论文Generative Prompt Model for Weakly Supervised Object Localization
    2022年新一批获得能力评估CS认证证书的企业名单
    TiKV 源码阅读三部曲(三)写流程
    CMake 基础学习
    算法设计与分析 SCAU10346 带价值的作业安排问题
    【面试刷题】——什么是面向过程 什么是面向对象
    【Day30】LeetCode算法 [769. 最多能完成排序的块 ] [2131. 连接两字母单词得到的最长回文串]
    C++:内存管理
    一些常见分布-正态分布、对数正态分布、伽马分布、卡方分布、t分布、F分布等
  • 原文地址:https://blog.csdn.net/wan212000/article/details/126126537