1. 前言

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

下一篇会介绍 Go并发控制--Context

使用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内部维护了一个计数器:

  1. 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
  2. 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
  3. 每个goroutine执行结束通过Done()方法将计数器减1。
  4. 计数器变为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时检查“坐等协程”计数器并把“坐等协程”唤醒。

需要注意

  1. Done()方法递减“工作协程”计数后,如果“工作协程”计数变成负数时,将会触发panic,这就要求Add()方法调用要早于Done()方法。

    也就是说代码中,如果调用Done的次数多于Add的次数会产生painc

  2. 当“工作协程”计数多于实际需要等待的“工作协程”数量时,“坐等协程”可能会永远无法被唤醒而产生列锁,此时,Go运行时检测到死锁会触发panic

    Add的添加的工作协程的数量,多于Done调用的次数,则会出现panic

  3. 当“工作协程”计数小于实际需要等待的“工作协程”数量时,Done()会在“工作协程”计数变为负数时触发panic。

    Add()添加的工作协程个数小于Done调用的次数,会出现panic

3. 总结

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

下一篇介绍Go并发控制--Context篇

Go并发控制--WaitGroup篇的更多相关文章

  1. Go并发控制--Channel篇

    目录 1. 前言 2. 使用channel控制子协程 2.1 使用场景 2.2 总结 1. 前言 我们考虑这么一种场景,协程A执行过程中需要创建子协程A1.A2.A3-An,协程A创建完子协程后就等待 ...

  2. 并发控制--context篇

    目录 1. 前言 2 Context 实现原理 2.1 接口定义 2.1 cancelCtx 2.1.1 Done()接口实现 2.1.2 Err()接口实现 2.1.3 cancel()接口实现 2 ...

  3. Go 并发控制--WaitGroup的使用

    开发过程中,经常task之间的同步问题.例如,多个子task并发完成一部分任务,主task等待他们最后结束. 在Go语言,实现同步的一种方式就是WaitGroup. Example package m ...

  4. go并发之goroutine和channel,并发控制入门篇

    并发的概念及其重要性 这段是简单科普,大佬可以跳过 并发:并发程序指同时进行多个任务的程序.在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行 ...

  5. 数据库的快照隔离级别(Snapshot Isolation)

    隔离级别定义事务处理数据读取操作的隔离程度,在SQL Server中,隔离级别只会影响读操作申请的共享锁(Shared Lock),而不会影响写操作申请的互斥锁(Exclusive Lock),隔离级 ...

  6. JDBC&&c3p0、事务、批处理、多线程 于一体的经典秘方QueryRunner

    目录: 基础篇_功能各自回顾 JDBC基础代码回顾(使用JdbcUtils工具简化) c3p0数据库连接池的使用(使用JdbcUtils工具简化) 大数据的插入(使用c3p0+JdbcUtils工具简 ...

  7. MySQL InnoDB下关于MVCC的一个问题的分析

      这个是网友++C++在群里问的一个关于MySQL的问题,本篇文章实验测试环境为MySQL 5.6.20,事务隔离级别为REPEATABLE-READ ,在演示问题前,我们先准备测试环境.准备一个测 ...

  8. 《JavaWeb从入门到改行》JDBC经典秘方QueryRunner

    目录: 基础篇_功能各自回顾 JDBC基础代码回顾(使用JdbcUtils工具简化) c3p0数据库连接池的使用(使用JdbcUtils工具简化) 大数据的插入(使用c3p0+JdbcUtils工具简 ...

  9. TGL站长关于常见问题的回复

    问题地址: http://www.thegrouplet.com/thread-112923-1-1.html 问题: 网站配有太多的模板是否影响网站加载速度 月光答复: wp不需要删除其他的模板,不 ...

随机推荐

  1. python -- 程序异常与调试(程序调试)

    一.程序调试 A.使用assert语句检测程序代码中的错误. assert 表达式[, 参数] 如果表达式为True,则继续往下运行:如果为False,则抛出一个AssertionError异常,并且 ...

  2. 【模拟+排序】花生采摘 luogu-1086

    题目描述 鲁宾逊先生有一只宠物猴,名叫多多.这天,他们两个正沿着乡间小路散步,突然发现路边的告示牌上贴着一张小小的纸条:"欢迎免费品尝我种的花生!――熊字". 鲁宾逊先生和多多都很 ...

  3. (6java)计算机语言发展史

    (6java)计算机语言发展史 机器语言: 程序是0和1的组合,比如:0000.0001.1100110 汇编语言: 程序比机器语言好理解一点点 高级语言: 比较适合老美,苦了英语差的孩子们了,哈哈. ...

  4. Spring Boot中使用时序数据库InfluxDB

    除了最常用的关系数据库和缓存之外,之前我们已经介绍了在Spring Boot中如何配置和使用MongoDB.LDAP这些存储的案例.接下来,我们继续介绍另一种特殊的数据库:时序数据库InfluxDB在 ...

  5. JS替换字符

    var msg='A|B|C|D|E|F|G'; 方式1: var newMsg=msg.replace("|",""); 方式2: ps:适用特殊字符 var ...

  6. 使用 Assimp 库加载 3D 模型

    前言 要想让自己的 3D 之旅多一点乐趣,肯定得想办法找一些有意思一点的 3D 模型.3D 模型有各种各样的格式,obj的,stl的,fbx的等等不一而足.特别是 obj 格式的 3D 模型,完全是纯 ...

  7. TCP三次握手、四次挥手理解

    tcp三次握手建立连接第一次握手 客户端发送给服务器一段连接请求报文,等待服务器回应 第二次握手 服务器收到报文,并发送给客户端一个确认报文,等待客户端回应 第三次握手 客户端收到新报文 ,再发送给服 ...

  8. Hello World!!

    已经工作了一年多,现在才开始写博客.话说,种一棵树最好的时机是十年前,其次是现在,我觉得不迟.俗话说滴水穿石,我想把一些东西,都慢慢积累起来,看见自己的成长.既方便查看,更不容易忘记.可能在网上已经有 ...

  9. 超详细 Java 15 新功能介绍

    点赞再看,动力无限.微信搜「程序猿阿朗 」,认认真真写文章. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. Java 15 在 2 ...

  10. 修改Linux系统的默认语言编码集

    RedHat 今天晚上发现服务器上vi的界面提示变成了乱码,只能将XShell的编码改为GBK才能正常显示,导致consolas字体无法使用,GBK编码下的字体丑陋无比,无法忍受,一轮google之后 ...