Go并发控制WaitGroup的使用场景分析

1. 前言

上一篇介绍了 Go并发控制--Channel

使用channel来控制子协程的优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制。

2. 使用WaitGroup控制

WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。比如某个goroutine需要等待其他几个goroutine全部完成,那么使用WaitGroup可以轻松实现。

2.1 使用场景

下面程序展示了一个goroutine等待另外两个goroutine结束的例子:

package main

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

func main() {
    var wg sync.WaitGroup

    wg.Add(2) //设置计数器,数值即为goroutine的个数
    go func() {
        //Do some work
        time.Sleep(1*time.Second)

        fmt.Println("Goroutine 1 finished!")
        wg.Done() //goroutine执行结束后将计数器减1
    }()

    go func() {
        //Do some work
        time.Sleep(2*time.Second)

        fmt.Println("Goroutine 2 finished!")
        wg.Done() //goroutine执行结束后将计数器减1
    }()

    wg.Wait() //主goroutine阻塞等待计数器变为0
    fmt.Printf("All Goroutine finished!")
}

简单的说,上面程序中wg内部维护了一个计数器:

  • 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
  • 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
  • 每个goroutine执行结束通过Done()方法将计数器减1。
  • 计数器变为0后,阻塞的goroutine被唤醒

其实WaitGroup也可以实现一组goroutine等待另一组goroutine,这有点像玩杂技,很容出错,如果不了解其实现原理更是如此。实际上,WaitGroup的实现源码非常简单。

2.2 信号量

信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源

可简单理解为信号量为一个数值:

  • 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1;
  • 当信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒;

1.3 WaitGroup 数据结构

源码包中src/sync/waitgroup.go:WaitGroup定义了其数据结构:

type WaitGroup struct {
    state1 [3]uint32
}

state1是个长度为3的数组,其中包含了state和一个信号量,而state实际上是两个计数器:

  • counter: 当前还未执行结束的goroutine计数器
  • waiter count: 等待goroutine-group结束的goroutine数量,即有多少个等候者
  • semaphore: 信号量

考虑到字节是否对齐,三者出现的位置不同,为简单起见,依照字节已对齐情况下,三者在内存中的位置如下所示:

WaitGroup对外提供三个接口:

  • Add(delta int): 将delta值加到counter中
  • Wait(): waiter递增1,并阻塞等待信号量semaphore
  • Done(): counter递减1,按照waiter数值释放相应次数信号量

下面分别介绍这三个函数的实现细节。

2.3.1 Add () 方法

Add()做了两件事,一是把delta值累加到counter中,因为delta可以为负值,也就是说counter有可能变成0或负值,所以第二件事就是当counter值变为0时,根据waiter数值释放等量的信号量,把等待的goroutine全部唤醒,如果counter变为负值,则panic.

Add()伪代码如下:

func (wg *WaitGroup) Add(delta int) {
    statep, semap := wg.state() //获取state和semaphore地址指针

    state := atomic.AddUint64(statep, uint64(delta)<<32) //把delta左移32位累加到state,即累加到counter中
    v := int32(state >> 32) //获取counter值
    w := uint32(state)      //获取waiter值

    if v < 0 {              //经过累加后counter值变为负值,panic
        panic("sync: negative WaitGroup counter")
    }

    //经过累加后,此时,counter >= 0
    //如果counter为正,说明不需要释放信号量,直接退出
    //如果waiter为零,说明没有等待者,也不需要释放信号量,直接退出
    if v > 0 || w == 0 {
        return
    }

    //此时,counter一定等于0,而waiter一定大于0(内部维护waiter,不会出现小于0的情况),
    //先把counter置为0,再释放waiter个数的信号量
    *statep = 0
    for ; w != 0; w-- {
        runtime_Semrelease(semap, false) //释放信号量,执行一次释放一个,唤醒一个等待者
    }
}

2.3.2 Wait()

Wait()方法也做了两件事,一是累加waiter, 二是阻塞等待信号量

func (wg *WaitGroup) Wait() {
    statep, semap := wg.state() //获取state和semaphore地址指针
    for {
        state := atomic.LoadUint64(statep) //获取state值
        v := int32(state >> 32)            //获取counter值
        w := uint32(state)                 //获取waiter值
        if v == 0 {                        //如果counter值为0,说明所有goroutine都退出了,不需要待待,直接返回
            return
        }

        // 使用CAS(比较交换算法)累加waiter,累加可能会失败,失败后通过for loop下次重试
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            runtime_Semacquire(semap) //累加成功后,等待信号量唤醒自己
            return
        }
    }
}

这里用到了CAS算法保证有多个goroutine同时执行Wait()时也能正确累加waiter。

2.3.3 Done()

Done()只做一件事,即把counter减1,我们知道Add()可以接受负值,所以Done实际上只是调用了Add(-1)。

源码如下:

func (wg *WaitGroup) Done() {
    wg.Add(-1)
}

Done()的执行逻辑就转到了Add(),实际上也正是最后一个完成的goroutine把等待者唤醒的。

2.4 总结

简单说来,WaitGroup通常用于等待一组“工作协程”结束的场景,其内部维护两个计数器,这里把它们称为“工作协程”计数器和“坐等协程”计数器,
WaitGroup对外提供的三个方法分工非常明确:

  • Add(delta int)方法用于增加“工作协程”计数,通常在启动新的“工作协程”之前调用;
  • Done()方法用于减少“工作协程”计数,每次调用递减1,通常在“工作协程”内部且在临近返回之前调用;
  • Wait()方法用于增加“坐等协程”计数,通常在所有”工作协

Done()方法除了负责递减“工作协程”计数以外,还会在“工作协程”计数变为0时检查“坐等协程”计数器并把“坐等协程”唤醒。

需要注意

  • Done()方法递减“工作协程”计数后,如果“工作协程”计数变成负数时,将会触发panic,这就要求Add()方法调用要早于Done()方法。
  • 也就是说代码中,如果调用Done的次数多于Add的次数会产生painc
  • 当“工作协程”计数多于实际需要等待的“工作协程”数量时,“坐等协程”可能会永远无法被唤醒而产生列锁,此时,Go运行时检测到死锁会触发panic
  • Add的添加的工作协程的数量,多于Done调用的次数,则会出现panic
  • 当“工作协程”计数小于实际需要等待的“工作协程”数量时,Done()会在“工作协程”计数变为负数时触发panic。
  • Add()添加的工作协程个数小于Done调用的次数,会出现panic

3. 总结

WaitGroup控制子协程的方式很简单,且目的很明确,等待一组子协程执行完毕再执行主线程,但是当子协程里面有子协程,子协程里面有其他的子协程时,这种并不知道有多少个子协程的情况下使用WaitGroup就很难,所以就需要****Context**上场了

到此这篇关于Go并发控制--WaitGroup篇的文章就介绍到这了,更多相关Go并发控制WaitGroup内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 基于django channel实现websocket的聊天室的方法示例

    websocket 网易聊天室? ​ web微信? ​ 直播? 假如你工作以后,你的老板让你来开发一个内部的微信程序,你需要怎么办?我们先来分析一下里面的技术难点 消息的实时性? 实现群聊 现在有这样一个需求,老板给到你了,关乎你是否能转正?你要怎么做? 我们先说消息的实时性,按照我们目前的想法是我需要用http协议来做,那么http协议怎么来做那? 是不是要一直去访问我们的服务器,问服务器有没有人给我发消息,有没有人给我发消息?那么大家认为我多长时间去访问一次服务比较合适那? 1分钟1次?1分

  • Go并发控制Channel使用场景分析

    1. 前言 channel一个类型管道,通过它可以在goroutine之间发送和接收消息.它是Golang在语言层面提供的goroutine间的通信方式. Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication). 它的操作符是箭头 <- . 我们考虑这么一种场景,协程A执行过程中需要创建子协程A1.A2.A3-An,协程A创建完子协程后就等待子协程退出. 针对这种场景,GO提供了三种解决方案: Channel:

  • Golang 实现分片读取http超大文件流和并发控制

    分片读取http超大文件流 Golang中的HTTP发送get请求,在获取内容有两种情况. Golang发送http get请求方式 resp, err := http.Get(sendUrl) if err != nil { fmt.Println("出错", err) return } 第一种方式是直接全部读取出来,这种方式在小数据量的时候很方便. body变量直接全部接收resp响应内容 body, err2 := ioutil.ReadAll(resp.Body) 第二种方式,

  • Django Channels 实现点对点实时聊天和消息推送功能

    简介在很多实际的项目开发中,我们需要实现很多实时功能:而在这篇文章中,我们就利用django channels简单地实现了点对点聊天和消息推送功能. 手边有一个项目需要用到后台消息推送和用户之间一对一在线聊天的功能.例如用户A评论了用户B的帖子,这时候用户B就应该收到一条通知,显示自己的帖子被评论了.这个功能可以由最基本的刷新页面后访问数据库来完成,但是这样会增加对后台服务器的压力,同时如果是手机客户端的话,也会造成流量的损失.于是,我们考虑使用websocket建立一个连接来完成这个功能. 但

  • Go 并发控制context实现原理剖析(小结)

    1. 前言 Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine. context翻译成中文是"上下文",即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文. 典型的使用场景如下图所示: 上图中由于goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,这种情况下

  • Go并发控制WaitGroup的使用场景分析

    1. 前言 上一篇介绍了 Go并发控制--Channel 使用channel来控制子协程的优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制. 2. 使用WaitGroup控制 WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束.比如某个goroutine需要等待其他几个goroutine全部完成,那么使用WaitGroup可以轻松实现. 2.1 使用场景 下面程序展示了一个g

  • 关于SQL数据库 msdb.dbo.sp_send_dbmail 函数发送邮件的场景分析

    在推行系统中,时不时会有用户提出希望系统能自动推送邮件,由于手头的工具和能力有限,不少需求都借助于sql server的邮件触发来实现. 步骤: 1.配置邮箱.步骤略,网上有不少帖子说明,手工直接在管理-数据库邮件配置即可.配置完成后可以右键测试邮箱是否正常工作. 2.制作发送邮件脚本 3.sql server 代理定义周期计划 邮件脚本编写: 场景一:业务部门希望可以每周提供一次样品库存,即将sql查询的结果以附件的方式发给指定的人员. EXEC msdb.dbo.sp_send_dbmail

  • Python 私有属性和私有方法应用场景分析

    类的私有属性和方法 Python是个开放的语言,默认情况下所有的属性和方法都是公开的 或者叫公有方法,不像C++和 Java中有明确的public,private 关键字来区分私有公有. Python默认的成员函数和成员变量都是公开的,类的私有属性指只有在类的内部使用的属性或方法,表现形式为以"__" 属性名或方法名以双下划线开头. class Test(object): __count = 0 # 私有属性 __count def get_count(self): return se

  • Webstorm解除版本控制的场景分析

    一.适用场景 有时我们想要复制一份远端check下来的代码在本地进行自由改动,但是如果还在svn或者git的版本控制下就会出现很多不必要的麻烦. 比如很多文件会是红色,新建文件的时候会很慢就像这样 二.解除方式 如果我们仅仅想简单的解除版本控制,推荐如下方式: 找到你项目所在文件夹,里面有个.idea文件夹 打开文件夹,有一个vcs.xml,这就是webstorm配置版本控制工具的配置文件 对其进行编辑 将 <mapping directory="$PROJECT_DIR$" v

  • vue router返回到指定的路由的场景分析

    项目场景: 项目场景:示例:A(商品详情)--B(商品购买页面)-C(支付成功页面)--D(订单页面) 问题描述: 提示:这里描述项目中遇到的问题: 如果我们不做控制的话,安卓按照浏览器返回机制,依次从D-C-B-A这样子,这样子会有一定的bug,测试那边也是说不过去啊,原本想利用beforeRouteLeave这个来操作进行更改跳转,发现还是有点问题最后还是用beforeRouteEnter来操作了,先放上两个的区别吧: 这里先介绍一下导航守卫 > beforeRouteEnter 离开路由之

  • Git提交代码错了吃后悔药的几种常用办法(场景分析)

    1. 前言 写代码都会犯错误,不小心提交了一个错误我该怎么办?提交的描述信息我该怎么办?如果你也遇到这个问题,那么今天胖哥分享的这篇文章将非常适合你. 2. Commit 错了怎么办 放心都是有后悔药吃的,我们来看看. 场景一 假如你git commit了一堆代码,但是你发现本次Commit的描述信息有点随意,你可以这样: git commit --amend -m"新的提交消息" 场景二 你本来打算Commit五个文件,但是由于疏忽你Commit了四个.当然你可以再Commit一次来

  • 基于Vue3.0开发轻量级手机端弹框组件V3Popup的场景分析

    之前有分享一个vue2.x移动端弹框组件,今天给大家带来的是Vue3实现自定义弹框组件. V3Popup 基于vue3.x实现的移动端弹出框组件,集合msg.alert.dialog.modal.actionSheet.toast等多种效果.支持20+种自定义参数配置,旨在通过极简的布局.精简的调用方式解决多样化的弹框场景. v3popup 在开发之初参考借鉴了Vant3.ElementPlus等组件化思想.并且功能效果和之前vue2.0保持一致. ◆ 快速引入 在main.js中全局引入v3p

  • JDK1.8中的ConcurrentHashMap使用及场景分析

    ConcurrentHashMap 的初步使用及场景: ConcurrentHashMap 是 J.U.C 包里面提供的一个线程安全并且高效的 HashMap,所以ConcurrentHashMap 在并发编程的场景中使用的频率比较高,那么我们就从ConcurrentHashMap 的使用上以及源码层面来分析 ConcurrentHashMap 到底是如何实现安全性的 api 使用: ConcurrentHashMap 是 Map 的派生类,所以 api 基本和 Hashmap 是类似,主要就是

  • springAop实现权限管理数据校验操作日志的场景分析

    前言 作为一个写java的使用最多的轻量级框架莫过于spring,不管是老项目用到的springmvc,还是现在流行的springboot,都离不开spring的一些操作,我在面试的时候问的最多的spring的问题就是我们在平常的项目中使用spring最多的有哪几个点 在我看来无非就两个 spring的bean管理,说的高大上一点就是spring的ioc,di spring的AOP spring是一个很强大的轻量级框架,功能远不止这两点,但是我们用的最多的就是这两点. spring bean 管

随机推荐