type
status
date
slug
summary
tags
category
icon
password
1. 引言
在现代软件开发中,利用多核处理器的并发能力是提高程序性能的关键。Go语言,因其轻量级的goroutine和强大的内置并发支持,成为了处理并发任务的首选语言之一。
sync.WaitGroup
是Go语言标准库中的一个同步原语,用于等待一组goroutine完成。它在开发中广泛用于协调和同步goroutine的执行。2. sync.WaitGroup的基本使用
在Go语言的
sync
包中,WaitGroup
用于等待一组goroutine执行结束。它的基本使用方法如下:- 导入sync包
- WaitGroup的方法
Add(int)
:添加或减少等待goroutine的数量Done()
:相当于Add(-1)
Wait()
:阻塞直到所有goroutine执行完毕
- 示例代码
3. sync.WaitGroup的底层实现
3.1. 结构体解析
sync.WaitGroup
的结构体定义如下:3.1.1. noCopy
noCopy
字段是一个空结构体,它被用来防止WaitGroup
结构体被复制。在Go中,某些对象如果被复制,可能会引起运行时的并发错误或者状态不一致的问题。noCopy
不占用内存空间,但可以被go vet
工具用来检测结构体是否被复制,从而帮助开发者避免这类错误。3.1.2. state
state
字段是一个使用atomic.Uint64
类型的原子变量,它存储了两个非常重要的信息:高32位用来存储当前的goroutine计数器(counter
),低32位用来存储等待该WaitGroup
完成的goroutine的数量(waiter count
)。这种设计允许通过单个原子操作来更新这两个值,从而保证了操作的原子性和线程安全性。- 高32位(计数器):这部分记录了需要等待结束的goroutine的数量。每次调用
Add()
方法时,这个计数器会根据传入的参数增加或减少。当计数器达到0时,表示所有的goroutine都已经完成了它们的任务。
- 低32位(等待者计数):这部分记录了调用
Wait()
方法并进入阻塞状态的goroutine的数量。这个计数在Wait()
方法中增加,在所有goroutine完成后(即计数器为0时)会逐一唤醒这些等待的goroutine。
3.1.3. sema
sema
字段是一个信号量,用于实现goroutine之间的同步。当WaitGroup
的计数器不为0时,调用Wait()
的goroutine会通过这个信号量进入休眠状态,等待被唤醒。当计数器变为0时,所有在此信号量上等待的goroutine将被唤醒。这个信号量是通过底层的系统调用(如runtime_Semacquire
和runtime_Semrelease
)来操作的,确保了等待和唤醒操作的正确性和效率。这三个字段共同协作,使得
sync.WaitGroup
能够准确地控制一组goroutine的执行流程,直到它们全部完成。通过原子操作和信号量的使用,WaitGroup
提供了一种高效且线程安全的方式来同步goroutine的结束,是Go并发编程中非常重要的工具。3.2. Add() 方法实现
Add()
方法是通过原子操作来改变state的。如果增加后的计数器变为0,它会检查是否有goroutine在等待,如果有,它将唤醒所有等待的goroutine。处理数据竞争检测
如果数据竞争检测(由
race
包提供支持)是启用的,那么在对WaitGroup
进行修改前后会有特定的处理:- 如果
delta
是负数,意味着有goroutine完成了它的任务,race.ReleaseMerge
函数会被调用来帮助检测潜在的数据竞争问题。
- 在修改操作进行的时候,数据竞争检测会被暂时禁用(
race.Disable()
),以避免在原子操作中报告错误的数据竞争,操作完成后再重新启用(defer race.Enable()
)。
原子地更新计数器
使用
atomic
包中的Add
方法,将delta
值左移32位(因为计数器存储在64位整数的高32位)并原子地加到state
上。这一步确保了在多个goroutine同时调用Add()
时,计数器的更新操作是线程安全的。检查计数器状态
- 计算新的计数器值:通过右移32位获取高32位的计数器值
v
。
- 获取等待计数:通过取低32位得到当前等待
Wait()
方法返回的goroutine数量w
。
- 如果计数器
v
变为负数,则抛出panic错误,因为这意味着有更多的Done()
调用(或等效的Add(-1)
)比Add()
调用,这是不合法的。
- 如果在
Wait()
并发调用的情况下错误地使用了Add()
(例如,在计数器已经为零的情况下仍然调用Add()
),也会抛出panic。
唤醒等待的goroutine(如果需要)
- 如果计数器
v
的值为0且存在等待的goroutine(w
不为0),则进入唤醒阶段。
- 首先确认在唤醒操作前没有其他goroutine修改了状态,这是通过比较
state
和重新加载的state
值来确认的。
- 重置
state
的值为0,清除所有等待计数。
- 使用循环逐个释放所有等待的goroutine,通过调用
runtime_Semrelease
函数,每次循环释放一个信号量,直到所有等待的goroutine都被唤醒。
这个方法的设计确保了
WaitGroup
在并发环境下的正确性和效率,使得goroutine的同步变得简单而可靠。3.3. Done() 方法实现
Done()
方法是Add(-1)
的简写形式,它表示一个goroutine完成了它的工作。3.4. Wait() 方法实现
Wait()
方法会阻塞调用它的goroutine直到计数器为0。如果在调用Wait()
时计数器已经是0,它将立即返回。处理数据竞争检测
如果数据竞争检测(由
race
包提供支持)是启用的,那么在Wait()
方法执行期间,数据竞争检测会被暂时禁用(race.Disable()
)。这是因为在等待期间,多个goroutine可能会同时操作WaitGroup
,而这些操作应当是安全的。检查计数器是否为零
- 首先使用
atomic.Load
方法原子地加载当前的state
值。
- 通过右移32位获取高32位的计数器值
v
(表示剩余未完成的goroutine数量)。
- 如果
v
为0,说明没有需要等待的goroutine了,当前goroutine可以立即返回而无需阻塞。
增加等待计数并尝试阻塞
- 如果计数器不为0,那么当前goroutine需要等待。此时,将等待计数(存储在
state
的低32位)增加1,这是通过原子比较并交换操作CompareAndSwap
完成的,确保更新操作的原子性。
- 在成功增加等待计数后,如果这是第一个进入等待状态的goroutine(即之前没有其他goroutine在等待),则使用
race
包中的Write
函数来处理数据竞争检测的同步问题。
阻塞当前goroutine
- 使用
runtime_Semacquire
函数来阻塞当前goroutine,直到其他地方(通常是Add()
方法中计数器变为0时)调用runtime_Semrelease
释放信号量。
- 在被唤醒后(即其他goroutine调用了
Done()
使计数器减到0),再次检查state
的值确保计数器确实为0。如果不为0,则表示WaitGroup
被错误地重用了,这时会抛出panic错误。
重新启用数据竞争检测并返回
- 如果使用了数据竞争检测,此时会重新启用检测(
race.Enable()
),并通过race.Acquire
来同步内存模型,确保当前goroutine看到的是最新的内存状态。
- 最后,
Wait()
方法返回,当前goroutine继续执行后续的代码。
通过以上步骤,
Wait()
方法确保了只有当WaitGroup
中所有的goroutine都调用了Done()
之后,阻塞在Wait()
方法上的goroutine才会继续执行。这种机制非常适合管理和同步多个goroutine的执行流程。4. 总结
这种设计使得
WaitGroup
能够在多个goroutine间安全且高效地同步状态,确保所有goroutine都完成后才继续执行。这些底层实现细节揭示了sync.WaitGroup
如何有效地管理并发任务的结束,保证了并发编程的正确性和效率。这篇文章详细介绍了Go语言中sync.WaitGroup的实现和使用。主要包含以下几个方面:
- 基本概念:sync.WaitGroup是Go语言的一个同步原语,用于等待一组goroutine完成执行。
- 基本使用方法:包含三个主要方法:
- Add(int):添加等待的goroutine数量
- Done():标记一个goroutine完成
- Wait():阻塞等待所有goroutine完成
- 底层实现:
- 结构包含noCopy(防止复制)和state1数组(存储状态)
- state1数组存储了计数器值和等待的goroutine数量
- 通过原子操作确保并发安全
正确使用
sync.WaitGroup
可以有效地协调多个goroutine之间的执行顺序,是Go并发编程中不可或缺的工具。- Author:iLikeBug
- URL:http://ilikebug.blog/Golang/golang-sync-waitgroup
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!