golang WaitGroup的使用与底层实现

2023-12-02 19:45

本文主要是介绍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/446570

相关文章

基于Python实现一个图片拆分工具

《基于Python实现一个图片拆分工具》这篇文章主要为大家详细介绍了如何基于Python实现一个图片拆分工具,可以根据需要的行数和列数进行拆分,感兴趣的小伙伴可以跟随小编一起学习一下... 简单介绍先自己选择输入的图片,默认是输出到项目文件夹中,可以自己选择其他的文件夹,选择需要拆分的行数和列数,可以通过

Python中将嵌套列表扁平化的多种实现方法

《Python中将嵌套列表扁平化的多种实现方法》在Python编程中,我们常常会遇到需要将嵌套列表(即列表中包含列表)转换为一个一维的扁平列表的需求,本文将给大家介绍了多种实现这一目标的方法,需要的朋... 目录python中将嵌套列表扁平化的方法技术背景实现步骤1. 使用嵌套列表推导式2. 使用itert

使用Docker构建Python Flask程序的详细教程

《使用Docker构建PythonFlask程序的详细教程》在当今的软件开发领域,容器化技术正变得越来越流行,而Docker无疑是其中的佼佼者,本文我们就来聊聊如何使用Docker构建一个简单的Py... 目录引言一、准备工作二、创建 Flask 应用程序三、创建 dockerfile四、构建 Docker

Python使用vllm处理多模态数据的预处理技巧

《Python使用vllm处理多模态数据的预处理技巧》本文深入探讨了在Python环境下使用vLLM处理多模态数据的预处理技巧,我们将从基础概念出发,详细讲解文本、图像、音频等多模态数据的预处理方法,... 目录1. 背景介绍1.1 目的和范围1.2 预期读者1.3 文档结构概述1.4 术语表1.4.1 核

Python使用pip工具实现包自动更新的多种方法

《Python使用pip工具实现包自动更新的多种方法》本文深入探讨了使用Python的pip工具实现包自动更新的各种方法和技术,我们将从基础概念开始,逐步介绍手动更新方法、自动化脚本编写、结合CI/C... 目录1. 背景介绍1.1 目的和范围1.2 预期读者1.3 文档结构概述1.4 术语表1.4.1 核

在Linux中改变echo输出颜色的实现方法

《在Linux中改变echo输出颜色的实现方法》在Linux系统的命令行环境下,为了使输出信息更加清晰、突出,便于用户快速识别和区分不同类型的信息,常常需要改变echo命令的输出颜色,所以本文给大家介... 目python录在linux中改变echo输出颜色的方法技术背景实现步骤使用ANSI转义码使用tpu

Conda与Python venv虚拟环境的区别与使用方法详解

《Conda与Pythonvenv虚拟环境的区别与使用方法详解》随着Python社区的成长,虚拟环境的概念和技术也在不断发展,:本文主要介绍Conda与Pythonvenv虚拟环境的区别与使用... 目录前言一、Conda 与 python venv 的核心区别1. Conda 的特点2. Python v

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

C#中Guid类使用小结

《C#中Guid类使用小结》本文主要介绍了C#中Guid类用于生成和操作128位的唯一标识符,用于数据库主键及分布式系统,支持通过NewGuid、Parse等方法生成,感兴趣的可以了解一下... 目录前言一、什么是 Guid二、生成 Guid1. 使用 Guid.NewGuid() 方法2. 从字符串创建

Python使用python-can实现合并BLF文件

《Python使用python-can实现合并BLF文件》python-can库是Python生态中专注于CAN总线通信与数据处理的强大工具,本文将使用python-can为BLF文件合并提供高效灵活... 目录一、python-can 库:CAN 数据处理的利器二、BLF 文件合并核心代码解析1. 基础合