Sync之WaitGroup模块源码分析
前言
WaitGroup和channel一样,也是Golang应用开发过程中经常使用的并发控制技术,不过它和channel实现的机制不一样,它是使用信号量来控制的。
使用示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(10) //Add的数量必须和for循环的次数保持一致
for i := 0; i < 10; i++ {
go func(i int) {
time.Sleep(1 * time.Second)
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("done")
}
结构体
这里以go1.19版本为例:
type WaitGroup struct {
noCopy noCopy
state1 uint64
state2 uint32
}
noCopy:golang的一种防拷贝技术,如果有被拷贝则会报错,所以我们在初始化waitGroup实例后就是一个全局对象,不能被拷贝
state1: 用于存放任务计数器和等待者计数器
state2:信号量的地址,和select
的结构体一样,也用到了信号量技术来控制线程的阻塞和唤醒
结构体实现了三个方法:Add()、Done()、Wait(),下面我看来分别看看它们的作用。
Add()方法
func (wg *WaitGroup) Add(delta int) {
// 获取state1和semap的指针地址
statep, semap := wg.state()
if race.Enabled {
_ = *statep // trigger nil deref early
if delta < 0 {
// Synchronize decrements with Wait.
race.ReleaseMerge(unsafe.Pointer(wg))
}
race.Disable()
defer race.Enable()
}
// 把delta左移32位累加到state,即累加到counter中
state := atomic.AddUint64(statep, uint64(delta)<<32)
// 获取任务counter的值
v := int32(state >> 32)
// 获取等待者的个数
w := uint32(state)
println("v=",v, "w=", w, "semap=", *semap)
if race.Enabled && delta > 0 && v == int32(delta) {
race.Read(unsafe.Pointer(semap))
}
if v < 0 {
panic("sync: negative WaitGroup counter")
}
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
// 如果任务counter数大于0或等待者个数等于0,则不阻塞直接返回
if v > 0 || w == 0 {
return
}
if *statep != state {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
// Reset waiters count to 0.
*statep = 0
// 走到这里说明任务都执行完了,并且还有等待者,因此会逐个释放信号量
for ; w != 0; w-- {
runtime_Semrelease(semap, false, 0)
}
}
Add方法会做两件事情:
1、 更新任务counter个数,可增可减;
2、如果任务都执行完且还有等待者,则逐个唤醒等待者。
Done()方法
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
每执行一次Done芳芳,则将任务数减1,同时判断是否需要唤醒等待者,参考Add()方法。
Wait()方法
func (wg *WaitGroup) Wait() {
// 获取state1和semap的指针地址
statep, semap := wg.state()
if race.Enabled {
_ = *statep // trigger nil deref early
race.Disable()
}
for {
state := atomic.LoadUint64(statep)
// 获取当前的任务counter数
v := int32(state >> 32)
// 获取当前的等待者个数
w := uint32(state)
if v == 0 {
// Counter is 0, no need to wait.
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(wg))
}
return
}
// 将当前等待者个数加1
if atomic.CompareAndSwapUint64(statep, state, state+1) {
if race.Enabled && w == 0 {
race.Write(unsafe.Pointer(semap))
}
// 一直阻塞直到获取到信号量
runtime_Semacquire(semap)
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(wg))
}
return
}
}
}
Wait方法会做两件事:
1、不断地尝试将等待者个数加1(CAS操作);
2、一旦增加成功,则一直阻塞直到获取到信号量。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性