Go语言之sync包 WaitGroup的使用
WaitGroup 是什么以及它能为我们解决什么问题?
WaitGroup在go语言中,用于线程同步,单从字面意思理解,wait等待的意思,group组、团队的意思,WaitGroup就是指等待一组,等待一个系列执行完成后才会继续向下执行。
正常情况下,goroutine的结束过程是不可控制的,我们可以保证的只有main goroutine的终止。
这时候可以借助sync包的WaitGroup来判断goroutine是否完成。
WaitGroup介绍
WatiGroup是sync包中的一个struct类型,用来收集需要等待执行完成的goroutine。下面是它的定义:
// WaitGroup用于等待一组线程的结束。
// 父线程调用Add方法来设定应等待的线程的数量。
// 每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
type WaitGroup struct {
// 包含隐藏或非导出字段
}
// Add方法向内部计数加上delta,delta可以是负数;
// 如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放,如果计数器小于0,方法panic。
// 注意Add加上正数的调用应在Wait之前,否则Wait可能只会等待很少的线程。
// 一般来说本方法应在创建新的线程或者其他应等待的事件之前调用。
func (wg *WaitGroup) Add(delta int)
// Done方法减少WaitGroup计数器的值,应在线程的最后执行。
func (wg *WaitGroup) Done()
// Wait方法阻塞直到WaitGroup计数器减为0。
func (wg *WaitGroup) Wait()
它有3个方法:
Add():每次激活想要被等待完成的goroutine之前,先调用Add(),用来设置或添加要等待完成的goroutine数量
例如Add(2)或者两次调用Add(1)都会设置等待计数器的值为2,表示要等待2个goroutine完成
Done():每次需要等待的goroutine在真正完成之前,应该调用该方法来人为表示goroutine完成了,该方法会对等待计数器减1
Wait():在等待计数器减为0之前,Wait()会一直阻塞当前的goroutine
也就是说,Add()用来增加要等待的goroutine的数量,Done()用来表示goroutine已经完成了,减少一次计数器,Wait()用来等待所有需要等待的goroutine完成。
示例一
package main
import (
"fmt"
"sync"
"time"
)
// 每个协程都会运行该函数。
// 注意,WaitGroup 必须通过指针传递给函数。
func worker(id int, wg *sync.WaitGroup) {
fmt.Printf("Worker %d starting\n", id)
// 睡眠一秒钟,以此来模拟耗时的任务。
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
// 通知 WaitGroup ,当前协程的工作已经完成。
wg.Done()
}
func main() {
// 这个 WaitGroup 被用于等待该函数开启的所有协程。
var wg sync.WaitGroup
// 开启几个协程,并为其递增 WaitGroup 的计数器。
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 阻塞,直到 WaitGroup 计数器恢复为 0,即所有协程的工作都已经完成。
wg.Wait()
}
main中开启了5个协程,开启协程之前都先调用了Add()方法增加了一个需要等待goroutine计数。每个goroutine都运行worker()函数,这个函数执行完成后调用Done()方法通知 WaitGroup表示当前协程的完成。
有一点需要注意,worker()函数中使用了指针类型的*sync.WaitGroup作为参数,这里不能使用值类型的sync.WaitGroup作为参数,因为这意味着每个goroutine都拷贝一份wg,每个goroutine都使用自己的wg。这显然是不合理的,这5个协程应该共享一个wg,这样才能知道这几个协程都完成了。实际上,如果使用值类型的参数,main goroutine将会永久阻塞而导致产生死锁。
还有一点需要注意Add和Done函数一定要配对,否则可能发生死锁,所报的错误信息如下:
fatal error: all goroutines are asleep - deadlock!
运行:
go run waitgroups.go
Worker 5 starting
Worker 3 starting
Worker 4 starting
Worker 1 starting
Worker 2 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done
每次运行,各个协程开启和完成的时间可能是不同的。
示例二
在工作中使用时,等待一个协程组全部正确完成则结束;但其中一个协程发生错误,这时候就会阻塞了,不推荐这种用法。
这种场景就需要使用到通知机制,这时候可以使用channel来实现。
package main
import (
"fmt"
"sync"
"time"
)
func main(){
// 这个 WaitGroup 被用于等待该函数开启的所有协程。
var wg sync.WaitGroup
// Add()方法开启了3个等待的协程计数
wg.Add(3)
// 开启3个协程,用于工作处理
go work1(&wg)
go work2(&wg)
go work3(&wg)
// 阻塞,直到 WaitGroup 计数器恢复为 0,即所有协程的工作都已经完成。
wg.Wait()
}
func work1(wg *sync.WaitGroup){
fmt.Println("work1 starting")
// 睡眠一秒钟,以此来模拟耗时的任务。
time.Sleep(time.Second)
fmt.Println("work1 done")
// 通知 WaitGroup ,当前协程的工作已经完成。
wg.Done()
}
func work2(wg *sync.WaitGroup){
fmt.Println("work2 starting")
// 睡眠一秒钟,以此来模拟耗时的任务。
time.Sleep(time.Second)
fmt.Println("work2 done")
// 通知 WaitGroup ,当前协程的工作已经完成。
wg.Done()
}
func work3(wg *sync.WaitGroup){
fmt.Println("work3 starting")
// 睡眠一秒钟,以此来模拟耗时的任务。
time.Sleep(time.Second)
fmt.Println("work3 done")
// 通知 WaitGroup ,当前协程的工作已经完成。
wg.Done()
}
源码分析
type WaitGroup struct {
noCopy noCopy
// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
// 64-bit atomic operations require 64-bit alignment, but 32-bit
// compilers do not ensure it. So we allocate 12 bytes and then use
// the aligned 8 bytes in them as state, and the other 4 as storage
// for the sema.
state1 [3]uint32
}
WaitGroup 结构十分简单,由 nocopy 和 state1 两个字段组成,其中 nocopy 是用来防止复制的
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
由于嵌入了 nocopy 所以在执行 go vet 时如果检查到 WaitGroup 被复制了就会报错。这样可以一定程度上保证 WaitGroup 不被复制,对了直接 go run 是不会有错误的,所以我们代码 push 之前都会强制要求进行 lint 检查,在 ci/cd 阶段也需要先进行 lint 检查,避免出现这种类似的错误。
~/project/Go-000/Week03/blog/06_waitgroup/02 main*
❯ go run ./main.go
~/project/Go-000/Week03/blog/06_waitgroup/02 main*
❯ go vet .
# github.com/mohuishou/go-training/Week03/blog/06_waitgroup/02
./main.go:7:9: assignment copies lock value to wg2: sync.WaitGroup contains sync.noCopy
state1 的设计非常巧妙,这是一个是十二字节的数据,这里面主要包含两大块,counter 占用了 8 字节用于计数,sema 占用 4 字节用做信号量
为什么要这么搞呢?直接用两个字段一个表示 counter,一个表示 sema 不行么?
不行,我们看看注释里面怎么写的。
// 64-bit value: high 32 bits are counter, low 32 bits are waiter count. > // 64-bit atomic operations require 64-bit alignment, but 32-bit > // compilers do not ensure it. So we allocate 12 bytes and then use > // the aligned 8 bytes in them as state, and the other 4 as storage > // for the sema.
这段话的关键点在于,在做 64 位的原子操作的时候必须要保证 64 位(8 字节)对齐,如果没有对齐的就会有问题,但是 32 位的编译器并不能保证 64 位对齐所以这里用一个 12 字节的 state1 字段来存储这两个状态,然后根据是否 8 字节对齐选择不同的保存方式。
这个操作巧妙在哪里呢?
- 如果是 64 位的机器那肯定是 8 字节对齐了的,所以是上面第一种方式
- 如果在 32 位的机器上
如果恰好 8 字节对齐了,那么也是第一种方式取前面的 8 字节数据
如果是没有对齐,但是 32 位 4 字节是对齐了的,所以我们只需要后移四个字节,那么就 8 字节对齐了,所以是第二种方式
所以通过 sema 信号量这四个字节的位置不同,保证了 counter 这个字段无论在 32 位还是 64 为机器上都是 8 字节对齐的,后续做 64 位原子操作的时候就没问题了。
这个实现是在 state 方法实现的
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
state 方法返回 counter 和信号量,通过 uintptr(unsafe.Pointer(&wg.state1))%8 == 0 来判断是否 8 字节对齐
Add
func (wg *WaitGroup) Add(delta int) {
// 先从 state 当中把数据和信号量取出来
statep, semap := wg.state()
// 在 waiter 上加上 delta 值
state := atomic.AddUint64(statep, uint64(delta)<<32)
// 取出当前的 counter
v := int32(state >> 32)
// 取出当前的 waiter,正在等待 goroutine 数量
w := uint32(state)
// counter 不能为负数
if v < 0 {
panic("sync: negative WaitGroup counter")
}
// 这里属于防御性编程
// w != 0 说明现在已经有 goroutine 在等待中,说明已经调用了 Wait() 方法
// 这时候 delta > 0 && v == int32(delta) 说明在调用了 Wait() 方法之后又想加入新的等待者
// 这种操作是不允许的
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
// 如果当前没有人在等待就直接返回,并且 counter > 0
if v > 0 || w == 0 {
return
}
// 这里也是防御 主要避免并发调用 add 和 wait
if *statep != state {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
// 唤醒所有 waiter,看到这里就回答了上面的问题了
*statep = 0
for ; w != 0; w-- {
runtime_Semrelease(semap, false, 0)
}
}
Wait
wait 主要就是等待其他的 goroutine 完事之后唤醒
func (wg *WaitGroup) Wait() {
// 先从 state 当中把数据和信号量的地址取出来
statep, semap := wg.state()
for {
// 这里去除 counter 和 waiter 的数据
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
// counter = 0 说明没有在等的,直接返回就行
if v == 0 {
// Counter is 0, no need to wait.
return
}
// waiter + 1,调用一次就多一个等待者,然后休眠当前 goroutine 等待被唤醒
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(semap)
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
return
}
}
}
Done
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
总结
通过WaitGroup提供的三个函数:Add,Done,Wait,可以轻松实现等待某个协程或协程组完成的同步操作。但在使用时要注意:
WaitGroup可以用于一个goroutine等待多个goroutine干活完成,也可以多个goroutine等待一个goroutine干活完成,是一个多对多的关系
多个等待一个的典型案例是 singleflight,这个在后面将微服务可用性的时候还会再讲到,感兴趣可以看看源码Add(n>0)方法应该在启动goroutine之前调用,然后在goroution内部调用Done方法WaitGroup必须在Wait方法返回之后才能再次使用Done只是Add的简单封装,所以实际上是可以通过一次加一个比较大的值减少调用,或者达到快速唤醒的目的。- 协程函数要使用指针类型的
*sync.WaitGroup作为参数,不能使用值类型的sync.WaitGroup作为参数 - Add的数量和Done的调用数量必须相等,否则可能发生死锁
WaitGroup在需要等待多个任务结束再返回的业务来说还是很有用的,但现实中用的更多的可能是,先等待一个协程组,若所有协程组都正确完成,则一直等到所有协程组结束;若其中有一个协程发生错误,则告诉协程组的其他协程,全部停止运行(本次任务失败)以免浪费系统资源。
该场景WaitGroup是无法实现的,那么该场景该如何实现呢,就需要用到通知机制,其实也可以用channel来实现,具体的解决办法,请看后续的文章。
这样说来,WaitGroup的使用场景是有限的。
Go语言之sync包 WaitGroup的使用的更多相关文章
- golang 中 sync包的 WaitGroup
golang 中的 sync 包有一个很有用的功能,就是 WaitGroup 先说说 WaitGroup 的用途:它能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 ...
- go语言中sync包和channel机制
文章转载至:https://www.bytelang.com/article/content/A4jMIFmobcA= golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"Go&quo ...
- Go中sync包学习
前面刚讲到goroutine和channel,通过goroutine启动一个协程,通过channel的方式在多个goroutine中传递消息来保证并发安全.今天我们来学习sync包,这个包是Go提供的 ...
- 深度解密 Go 语言之 sync.Pool
最近在工作中碰到了 GC 的问题:项目中大量重复地创建许多对象,造成 GC 的工作量巨大,CPU 频繁掉底.准备使用 sync.Pool 来缓存对象,减轻 GC 的消耗.为了用起来更顺畅,我特地研究了 ...
- R语言︱H2o深度学习的一些R语言实践——H2o包
每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- R语言H2o包的几个应用案例 笔者寄语:受启发 ...
- Go语言基础之包
Go语言基础之包 在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的.本文介绍了Go语言中如何定义包.如何导出包的内容及如何导入其他包. Go语言的包(packag ...
- R语言:recommenderlab包的总结与应用案例
R语言:recommenderlab包的总结与应用案例 1. 推荐系统:recommenderlab包整体思路 recommenderlab包提供了一个可以用评分数据和0-1数据来发展和测试推荐算 ...
- 使用R语言的RTCGA包获取TCGA数据--转载
转载生信技能树 https://mp.weixin.qq.com/s/JB_329LCWqo5dY6MLawfEA TCGA数据源 - R包RTCGA的简单介绍 - 首先安装及加载包 - 指定任意基因 ...
- sync包 — 汇总
sync包 package main; import ( "time" "fmt" ) func main() { //time.Time代表一个纳秒精度的时间 ...
- Go语言内置包之strconv
文章引用自 Go语言内置包之strconv Go语言中strconv包实现了基本数据类型和其字符串表示的相互转换. strconv包 strconv包实现了基本数据类型与其字符串表示的转换,主要有以下 ...
随机推荐
- Qt编写物联网管理平台50-超强跨平台
一.前言 跨平台的需求,除了是用户的需求外,也是为了适应日益增长的国产操作系统的发展的需要,当前国产操作系统发展的如火如荼,100%都是围绕linux系统展开,说的好听点就是站在巨人的肩膀上开发,不好 ...
- [转]Automatic Image Stitching with Accord.NET
原文链接:Automatic Image Stitching with Accord.NET
- 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-9- 浏览器的相关操作 (详细教程)
1.简介 在自动化测试领域,元素定位是非常重要的一环.正确定位页面元素是测试用例能否成功执行的关键因素之一.playwright是一种自动化测试工具,它提供了丰富的元素定位方法,可以满足不同场景下的定 ...
- AngleSharp 自带的HttpRequest参数设置
AngleSharp自带一个获取网址源码的api,可以方便的从web取得html var config = Configuration.Default.WithDefaultLoader(); var ...
- Appium_WebDriverAgent设置
在使用真机调试的时候犯了一个错误,我把WebDriverAgent 下载到本地的A目录下,然后进行build安装,这样在模拟器上执行是无法发现问题的,但是使用appium 在真机上执行 ...
- Ps cs4 -把GIF背景变透明-简单操作,还可以将视频转换GIF
准备软件: 1.Ps cs4 2.QuickTime Player 7.74 开始: 1. 2.弹出文件选择框,但是发现不能选择GIF格式. 3.没关系,在文件名框输入*.*回车,就发现可以选择GIF ...
- HttpClient.PostAsynct 发送Json数据
HttpClient.PostAsync第二个参数设置HttpContent 发送Json数据. 需要这是这个content.Headers.ContentType = new System.Net. ...
- biancheng-Redis教程
目录http://c.biancheng.net/redis/ 1Redis是什么2Windows下载安装Redis3Ubuntu下载安装Redis4Redis配置文件5Redis数据类型6Redis ...
- bug的合规描述
bug的合格描述: 发现问题的版本bug的合格描述: 开发人员需要知道出现问题的版本,才能够获取对应版本的代码来重现故障问题出现的环境 环境分为硬件环境和软件环境,详细的环境描述有利于故障的重现( ...
- 牛客周赛 Round 77
题目链接:牛客周赛 Round 77 A. 时间表 tag:签到 B. 数独数组 tag:签到 Description:给定n个数,每个数的范围为1-9,问能否经过排列,使其每个长度为9的连续子数组都 ...