• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Golang sync.WaitGroup

武飞扬头像
云满笔记
帮助2

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}

上一节我们学习过, 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, 然后就会被唤醒}

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.4. 总结

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbhhfh
系列文章
更多 icon
同类精品
更多 icon
继续加载