专栏首页学院君的专栏Go 语言并发编程系列(十三)—— sync 包系列:sync.WaitGroup 和 sync.Once

Go 语言并发编程系列(十三)—— sync 包系列:sync.WaitGroup 和 sync.Once

在介绍通道的时候,如果启用了多个子协程,我们是这样实现主协程等待子协程执行完毕并退出的:声明一个和子协程数量一致的通道数组,然后为每个子协程分配一个通道元素,在子协程执行完毕时向对应的通道发送数据;然后在主协程中,我们依次读取这些通道接收子协程发送的数据,只有所有通道都接收到数据才会退出主协程。

代码看起来是这样的:

chs := make([]chan int, 10)for i := 0; i < 10; i++ {    chs[i] = make(chan int)    go add(1, i, chs[i])}for _, ch := range chs {    <- ch}

我总感觉这样的实现有点蹩脚,不够优雅,不知道你有没有同感,那有没有更好的实现呢?这就要引入我们今天要讨论的主题:sync 包提供的 sync.WaitGroup 类型。

sync.WaitGroup 类型

sync.WaitGroup 类型是开箱即用的,也是并发安全的。该类型提供了以下三个方法:

  • AddWaitGroup 类型有一个计数器,默认值是0,我们可以通过 Add 方法来增加这个计数器的值,通常我们可以通过个方法来标记需要等待的子协程数量;
  • Done:当某个子协程执行完毕后,可以通过 Done 方法标记已完成,该方法会将所属 WaitGroup 类型实例计数器值减一,通常可以通过 defer 语言来调用它;
  • WaitWait 方法的作用是阻塞当前协程,直到对应 WaitGroup 类型实例的计数器值归零,如果在该方法被调用的时候,对应计数器的值已经是 0,那么它将不会做任何事情。

至此,你可能已经看出来了,我们完全可以组合使用 sync.WaitGroup 类型提供的方法来替代之前通道中等待子协程执行完毕的实现方法,对应代码如下:

package main
import (    "fmt"    "sync")
func add_num(a, b int, deferFunc func()) {    defer func() {        deferFunc()    }()    c := a + b    fmt.Printf("%d + %d = %d\n", a, b, c)}
func main() {    var wg sync.WaitGroup    wg.Add(10)    for i := 0; i < 10; i++ {        go add_num(i, 1, wg.Done)    }    wg.Wait()}

看起来代码简洁多了,我们首先在主协程中声明了一个 sync.WaitGroup 类型的 wg 变量,然后调用 Add 方法设置等待子协程数为 10,然后循环启动子协程,并将 wg.Done 作为 defer 函数传递过去,最后,我们通过 wg.Wait() 等到 sync.WaitGroup 计数器值为 0 时退出程序。

上述代码打印结果和之前通过通道实现的结果是一致的:

以上就是 sync.WaitGroup 类型的典型使用场景,通过它我们可以轻松实现一主多子的协程协作。需要注意的是,该类型计数器不能小于0,否则会抛出如下 panic:

panic: sync: negative WaitGroup counter

sync.Once 类型

sync.WaitGroup 类型类似,sync.Once 类型也是开箱即用和并发安全的,其主要用途是保证指定函数代码只执行一次,类似于单例模式,常用于应用启动时的一些全局初始化操作。它只提供了一个 Do 方法,该方法只接受一个参数,且这个参数的类型必须是 func(),即无参数无返回值的函数类型。

在具体实现时,sync.Once 还提供了一个 uint32 类型的 done 字段,它的作用是记录 Do 传入函数被调用次数,显然,其对应的值只能是 0 和 1,之所以设置为 uint32 类型,是为了保证操作的原子性,回想下我们上篇教程中介绍的原子函数,再结合 Do 方法底层实现源码,即可知晓原因,这里不深入探讨了:

func (o *Once) Do(f func()) {    if atomic.LoadUint32(&o.done) == 1 {        return    }    // Slow-path.    o.m.Lock()    defer o.m.Unlock()    if o.done == 0 {        defer atomic.StoreUint32(&o.done, 1)        f()    }}

如果 done 字段的值已经是 1 了(通过 atomic.LoadUint32() 原子加载),表示该函数已经调用过,否则的话会调用 sync.Once 提供的互斥锁阻塞其它代码对该类型的访问,然后通过原子操作将 done 的值设置为 1,并调用传入函数。

下面我们通过一个简单的示例来演示 sync.Once 类型的使用:

package main
import (    "fmt"    "sync"    "time")
func dosomething(o *sync.Once)  {    fmt.Println("Start:")    o.Do(func() {        fmt.Println("Do Something...")    })    fmt.Println("Finished.")}
func main()  {    o := &sync.Once{}    go dosomething(o)    go dosomething(o)    time.Sleep(time.Second * 1)}

上述代码的运行结果是:

显然,传入 sync.Once.Do 方法的函数只会被执行一次。

本文分享自微信公众号 - 学院君的后花园(geekacademy),作者:学院君

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于 gorilla/sessions 在 Go 语言中管理 Session

    Go 语言官方提供的 http 包虽然对 HTTP 编程提供了丰富的 API,但是没有提供官方的 Session 实现。如果在 Web 应用中使用到了 Sess...

    学院君
  • 玩转 PhpStorm 系列(十):代码调试篇(下)

    上篇教程我们演示了如何安装配置 Xdebug 扩展,并且在 PhpStorm 中基于 Xdebug 对 PHP CLI 脚本代码进行调试。不过 PHP 主要应用...

    学院君
  • Go 语言并发编程系列(十)—— sync 包系列:互斥锁和读写锁

    我们前面反复强调,在 Go 语言并发编程中,倡导「使用通信共享内存,不要使用共享内存通信」,而这个通信的媒介就是我们前面花大量篇幅介绍的通道(Channel),...

    学院君
  • Golang包——sync

    1.它允许任意读操作同时进行 2.同一时刻,只允许有一个写操作进行 3.并且一个写操作被进行过程中,读操作的进行也是不被允许的 4.读写锁控制下的多个写操...

    羊羽shine
  • 吴恩达机器学习笔记 —— 19 应用举例:照片OCR(光学字符识别)

    我们定义几个固定大小尺寸的窗口,从照片的左上角开始扫描。扫描出来的图像做二分类,判断是北京还是人物(文字)。然后根据图像处理的一些惯用手段做二值化、膨胀,使得文...

    用户1154259
  • 指针在液晶屏显示中的用法(一)

    这天,老板给了一个任务,给他们公司的产品增加一个液晶屏LCD1602,显示五个页面,可通过上下按键进行切换。

    MCU起航
  • Java虚拟机对内部锁的优化

    锁消除(Lock Elision)是JIT编译器对内部锁的具体实现所做的一种优化。

    博文视点Broadview
  • SAP Spartacus page-slot.component.html

    Jerry Wang
  • 首提跨模态代码匹配算法,腾讯安全科恩实验室论文入选国际AI顶会NeurIPS-2020

    人工智能领域顶级学术会议NeurIPS 2020(Neural Information Processing Systems)将于12月7日-12日在线上举行。...

    腾讯安全
  • 腾讯安全副总裁黎巍谈WAF:通过云原生能力构建安全基座

    在万物上云的新生态下,传统安全问题的变本加厉与新生安全威胁的杂糅,使得云安全需求已然成为整个产业升级发展的基础支撑。业务线上拓展使得数据信息价值攀升的背后,是由...

    腾讯安全

扫码关注云+社区

领取腾讯云代金券