Golang Sync.WaitGroup 使用及原理
Golang Sync.WaitGroup 使用及原理
使用
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Hello WaitGroup!")
}()
}
wg.Wait()
}
实现
首先看 waitgroup 到底是什么数据结构
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
nocopy 避免这个结构体被复制的一个技巧,可以告诉go vet工具违反了复制使用的规则
state1 [3]uint32 字段中包含了 waitgroup 的所有状态信息, 根据标准库上自带的注释简单翻译是:state1 由 12 个字节组成,其中将8字节看作64位值,其中高32位存放的是 counter 计数器, 代表目前还未完成的 goroutine个数,低32位存放的是 waiter 计数器, 可以理解成下面这个结构体
type WaitGroup struct {
// 代表目前尚未完成的个数
// WaitGroup.Add(n) 将会导致 counter += n
// WaitGroup.Done() 将导致 counter--
counter uint32
// 目前已调用 WaitGroup.Wait 的 goroutine 的个数
waiter uint32
// 对应于 golang 中 runtime 内部的信号量的实现
// runtime_Semacquire 表示增加一个信号量,并挂起当前 goroutine
// runtime_Semrelease 表示减少一个信号量,并唤醒 sema 上其中一个正在等待的 goroutine
sema uint32
}
整个使用流程为:
- 当调用
WaitGroup.Add(n)时,counter 将会自增:counter += n - 当调用
WaitGroup.Wait()时,会将waiter++。同时调用runtime_Semacquire(semap), 增加信号量,并挂起当前 goroutine。 - 当调用
WaitGroup.Done()时,将会counter--。如果自减后的 counter 等于 0,说明 WaitGroup 的等待过程已经结束,则需要调用runtime_Semrelease释放信号量,唤醒正在WaitGroup.Wait的 goroutine。
源码中是如何拆分 state 字段的
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
// 如果地址是64bit对齐的,数组前两个元素做state,后一个元素做信号量
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {
// 如果地址是32bit对齐的,数组后两个元素用来做state
// 它可以用来做64bit的原子操作,第一个元素32bit用来做信号量
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
由于我们能使用到的就是 waitgroup.Add(), waitgroup.Done(), waitgroup.Wait() 这三个方法,就按这三个方法分析
Add(), Done()
Add 方法主要操作的是 state 的计数部分。你可以为计数值增加一个 delta 值,内部通过原子操作把这个值加到计数值上。需要注意的是,这个 delta 也可以是个负数,相当于为计数值减去一个值,Done 方法内部其实就是通过 Add(-1) 实现的。
func (wg *WaitGroup) Add(delta int) {
// 获取拆开后的 state 字段
statep, semap := wg.state()
...
...
...
// 在刚刚说的 int64 的高32位上加伤传进来的 delta 的值, 这一步是原子操作
state := atomic.AddUint64(statep, uint64(delta)<<32)
// 加好后,获取 counter 也就是 v, 和 waiter 也就是 w 的值
// 此时 int64 变为两个 int32
v := int32(state >> 32)
w := uint32(state)
// 如果 v 变为负数了,程序异常
if v < 0 {
panic("sync: negative WaitGroup counter")
}
// 在 wait 没结束之前, 不允许调用 Add 方法
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
// 调用 add() 之后, 还有正在执行的 goroutine 或者 waiter 等于 0, 正常返回
if v > 0 || w == 0 {
return
}
// 下面就是非正常返回, 理解到的就是 v 已经等于 0 了,执行释放操作
// 首先就是将 counter 和 waiter 全部重置为 0
*statep = 0
// 然后循环调用还在等待的 waiter, 释放信号量
for ; w != 0; w-- {
runtime_Semrelease(semap, false, 0)
}
}
wait()
Wait 方法的实现逻辑是:不断检查 state 的值。如果其中的计数值变为了 0,那么说明所有的任务已完成,调用者不必再等待,直接返回。如果计数值大于 0,说明此时还有任务没完成,那么调用者就变成了等待者,需要加入 waiter 队列,并且阻塞住自己。
func (wg *WaitGroup) Wait() {
// 获取信号量和两个计数值
statep, semap := wg.state()
// 不停的循环检查 counter 和 waiter
for {
// 先原子性的取出 counter 和 waiter
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
if v == 0 {
// counter 已经没有了,函数可以返回
return
}
// 将 waiter 数 + 1
if atomic.CompareAndSwapUint64(statep, state, state+1) {
// 放到信号量队列, 并且阻塞住自己
runtime_Semacquire(semap)
// 如果被唤醒,检查 两个计数是否已经为0 了, 如果不为0 ,则触发恐慌
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
// 函数返回
return
}
}
}
总结
- 保证计数器不能为负值
- 保证 Add() 方法全部调用完成之后再调用 Wait()
- waitgroup 可以重复使用
- atomic 原子操作代替锁, 提高并发性
- 合并两个 int32 为一个 int64 提高读取存入数据性能
- 对于不希望被复制的结构体, 可以使用 noCopy 字段
reference
https://www.cyhone.com/articles/golang-waitgroup/
https://time.geekbang.org/column/intro/100061801?tab=catalog
Golang Sync.WaitGroup 使用及原理的更多相关文章
- golang sync.WaitGroup
//阻塞,直到WaitGroup中的所以过程完成. import ( "fmt" "sync" ) func wgProcess(wg *sync.WaitGr ...
- Golang sync.WaitGroup的用法
0x01 介绍 经常会看到以下了代码: 12345678910111213 package main import ( "fmt" "time") func m ...
- golang sync.WaitGroup bug
注意,这个结构体,要是想在函数之间传来传去的话,必须要使用指针....... 这个结构体里没有 指针,这个类型可以说没有“引用特性”. 被坑了一晚上.特此记录.
- golang-----golang sync.WaitGroup解决goroutine同步
go提供了sync包和channel来解决协程同步和通讯.新手对channel通道操作起来更容易产生死锁,如果时缓冲的channel还要考虑channel放入和取出数据的速率问题. 从字面就可以理解, ...
- golang 的 sync.WaitGroup
WaitGroup的用途:它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成. 官方对它的说明如下: A WaitGroup waits for ...
- Golang的sync.WaitGroup 实现逻辑和源码解析
在Golang中,WaitGroup主要用来做go Routine的等待,当启动多个go程序,通过waitgroup可以等待所有go程序结束后再执行后面的代码逻辑,比如: func Main() { ...
- golang的sync.WaitGroup使用示例
下面一段代码 len(m) 不一定会打印为 10,为什么?.如果想要 len(m) 打印为 10,应该怎么修改代码? func main() { const N = 10 m := make(map[ ...
- Go并发控制之sync.WaitGroup
WaitGroup 会将main goroutine阻塞直到所有的goroutine运行结束,从而达到并发控制的目的.使用方法非常简单,真心佩服创造Golang的大师们! type WaitGroup ...
- Golang中WaitGroup使用的一点坑
Golang中WaitGroup使用的一点坑 Golang 中的 WaitGroup 一直是同步 goroutine 的推荐实践.自己用了两年多也没遇到过什么问题.直到一天午睡后,同事扔过来一段奇怪的 ...
随机推荐
- PAT 乙级 1002. 写出这个数 (20)(C语言描述)
读入一个自然数n,计算其各位数字之和,用汉语拼音写出和的每一位数字. 输入格式:每个测试输入包含1个测试用例,即给出自然数n的值.这里保证n小于10100. 输出格式:在一行内输出n的各位数字之和的每 ...
- 【Java】File类
File类 File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹) File类声明在java.io包下 File类中涉及到关于文件或文件目录的创建.删除.重命名.修改时间.文件大小等方法,并 ...
- 【Java】多态性
文章目录 多态性 向下转型 多态性 可以理解为一个事物的多种形态. 对象的多态性:父类的引用指向子类的对象.只适用于方法,不适用于属性(编译和运行都看左边) 总结:对于对象的多态性,编译,看左边:运行 ...
- Java库中的LocalDate类
Java库中的LocalDate类 类库设计者决定将保存时间与给时间点命名分开.所以标准Java类库分别包含了两个类:一个用来表示时间点的Date类:另一个是用来表示大家熟悉的日历表示法的LocalD ...
- 新手学习Java,如何快速从入门到精通!
首先我要给你说的是,任何一个东西从零到精通,都是需要一个过程的,就相当于我们在小学的时候学乘法口诀表,背文言文一样,都是需要我们慢慢的去渡过这个阶段,我们才能慢慢的掌握,其次,毕竟这是一门编程语言,你 ...
- thanos的日志能不能打到文件里面去?
不行. thanos/pkg/logging/logger.go: logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) if logF ...
- 百度云管家使用socks代理无法上传下载解决办法
像前几篇随笔描述的那样,笔者在学校里通过shadowsocks使用ipv6访问双栈vps来免费使用外网,但是在设置百度云管家的代理时发现:使用socks代理客户端可以访问文件列表,但是无法上传下载. ...
- DispatcherServlet的init源代码
springmvc执行过程源代码分析 1,tomcat启动,创建容器的过程 通过load-on-start标签指定的1,创建DispatcherServlet对象, DispatcherServlet ...
- java接口应用
1 package face_09; 2 /* 3 * 笔记本电脑使用. 4 * 为了扩展笔记本的功能,但日后出现什么功能设备不知道. 5 * 6 * 定义了一个规则,只要日后出现的设备都符合这个规则 ...
- java-异常-异常处理原则
1 异常处理的原则: 2 * 1,函数内部如果抛出需要检测的异常,那么函数上必须要声明. 3 * 否则必须在函数内用trycatch捕捉,否则编译失败. 4 * 5 * 2,如果调用到了声明异常的函数 ...