golang WaitGroup的使用与底层实现

2023-11-30 12:28

本文主要是介绍golang WaitGroup的使用与底层实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用的go版本为 go1.21.2

首先我们写一个简单的WaitGroup的使用代码

package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()fmt.Println("xiaochuan")}()wg.Wait()
}

WaitGroup的基本使用场景就是等待子协程完毕后,执行主协程,比如我的api需要多个下游api支持开多个协程进行访问,等待耗时最高的api返回过来后执行,这种场景是比较适合WaitGroup的。

我们来看一下WaitGroup构造体相关的底层源码

WaitGroup结构体

//代码位于 GOROOT/src/sync/waitgroup.go L:23type WaitGroup struct {//防止WaitGroup被复制, 君子协议,编译可以通过,某些编辑器会报waring//有兴趣可以看一下这里 https://github.com/golang/go/issues/8005#issuecomment-190753527noCopy noCopy// 高32位表示计数器,低32位表示等待的waiter数量。// 低版本go的state字段类型是[3]uint32,需要进行位数对齐state atomic.Uint64// 信号量sema  uint32
}
编辑器的warning

Add函数

//代码位于 GOROOT/src/sync/waitgroup.go L:43func (wg *WaitGroup) Add(delta int) {if race.Enabled { //使用竞态检查if delta < 0 { //如果传递的数值是负数,递减等待同步// Synchronize decrements with Wait.race.ReleaseMerge(unsafe.Pointer(wg))}race.Disable() //竞态检查 禁用defer race.Enable() //竞态检查 启用}//计算我们要进行add的值,将其加入到比特位上//<< 32 为二进制左位移 32位state := wg.state.Add(uint64(delta) << 32)v := int32(state >> 32) // state变量的高位是计数w := uint32(state) // state变量的低位是waiter计数//使用竞态检查,当前传入的值与v相同,说明当前是第一次调度addif race.Enabled && delta > 0 && v == int32(delta) {// The first increment must be synchronized with Wait.// Need to model this as a read, because there can be// several concurrent wg.counter transitions from 0.race.Read(unsafe.Pointer(&wg.sema))}//如果 计数器小于0 说明了多进行了done操作或者add传递负数,业务代码的出现逻辑错误了if v < 0 {panic("sync: negative WaitGroup counter")}// 如果当前存在等待,而且计数器不为0// 说明当前有地方调度了Wait后,又进行add操作了, 违反了官方的使用设计if w != 0 && delta > 0 && v == int32(delta) {panic("sync: WaitGroup misuse: Add called concurrently with Wait")}// 计数大于0,没有等待,就是单纯的add直接返回if v > 0 || w == 0 {return}// 再做一次检测,防止有并发调度// 比如我有两个goroutine A goroutine 在add, B goroutine 在调度 wait // 刚刚好A加完了计数,B突然wait导致state更变就会触发这个panicif wg.state.Load() != state {panic("sync: WaitGroup misuse: Add called concurrently with Wait")}// 重置waiter为0wg.state.Store(0)for ; w != 0; w-- { // 逐步释放信号量runtime_Semrelease(&wg.sema, false, 0)}
}

Done函数

//代码位于 GOROOT/src/sync/waitgroup.go L:86//这个很简单 调用了一下add函数传了一个-1
func (wg *WaitGroup) Done() {wg.Add(-1)
}

Wait函数

//代码位于 GOROOT/src/sync/waitgroup.go L:91func (wg *WaitGroup) Wait() {if race.Enabled { //使用竞态检查race.Disable() //竞态检查 禁用}for {state := wg.state.Load() // 原子操作读取state字段v := int32(state >> 32) // state变量的高位是计数w := uint32(state) // state变量的低位是waiter计数if v == 0 { // 如果当前计数器为0 就没必要等待直接返回了if race.Enabled {race.Enable() //竞态检查 启用race.Acquire(unsafe.Pointer(wg))}return}// 将waiter计数+1 因为waiter处于低32位所以不需要位移直接加就行了if wg.state.CompareAndSwap(state, state+1) {if race.Enabled && w == 0 { // 使用竞态检查,第一次进行wait操作// Wait must be synchronized with the first Add.// Need to model this is as a write to race with the read in Add.// As a consequence, can do the write only for the first waiter,// otherwise concurrent Waits will race with each other.race.Write(unsafe.Pointer(&wg.sema))}// 获取信号量,这行代码会进行G的阻塞runtime_Semacquire(&wg.sema)//重新获取一下state,正常来讲计数为0, waiter为0//执行判断之前,又有一个协程进行了add操作,会触发panicif wg.state.Load() != 0 {panic("sync: WaitGroup is reused before previous Wait has returned")}if race.Enabled { //使用竞态检查race.Enable() //竞态检查 启用race.Acquire(unsafe.Pointer(wg))}return}}
}

总结

我们从上面的源码分析了解WaitGroup的数据结构、Add、Done和Wait这些基本操作原理,在项目中我们可以使用比特位来减少内存的占用,从源码分析我们得知Go官方设计不允许进行WaitGroup复制(君子协议)与并发调度同一个WaitGroup操作。

这篇关于golang WaitGroup的使用与底层实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/437027

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没