Go语言 WaitGroup 源码知多少

简介: Go语言 WaitGroup 源码知多少

前面的文章我们写协程的时候有用到 WaitGroup

我们的写法大概是这样的

func main() {
    ...dothing()
  wg := sync.WaitGroup{}
  // 控制 多个子协程的声明周期
  wg.Add(xx)
  for i := 0; i < xx; i++ {
    go func(ctx context.Context) {
      defer wg.Done()
      ...dothing()
    }(ctx)
  }
  ...dothing()
  // 等待所有的子协程都优雅关闭
  wg.Wait()
  fmt.Println("close server ")
}

可以看出,sync.WaitGroup 主要是用来等待一批协程关闭的,例如上面的 主协程 等待 所有子协程关闭,自己才进行退出

那么我们今天就来探究一下 sync.WaitGroup 的源码实现吧

探究源码实现

sync.WaitGroup 的使用上述 dmeo 已经给出,看上去用起来也很简单

使用 Add 函数是添加等待的协程数量

使用 Done 函数是通知 WaitGroup 当前协程任务完成了

使用 Wait 函数 是等待所有的子协程关闭

咱打开源码

源码路径:src/sync/waitgroup.go ,总共源码 141 行

单测文件 src/sync/waitgroup_test.go301 行

源码文件总共 4 个函数, 1 个结构体

  • type WaitGroup struct {
  • func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
  • func (wg *WaitGroup) Add(delta int) {
  • func (wg *WaitGroup) Done() {
  • func (wg *WaitGroup) Wait() {

我们逐个来瞅一瞅这几个函数都做了那些事情

type WaitGroup struct {

WaitGroup 等待一组 goroutine 完成,主 goroutine 调用 Add 来设置等待的 goroutines

然后是每一个协程调用 ,当完成时运行并调用 Done

与此同时,Wait 可以被用来阻塞,直到所有 goroutine 完成

WaitGroup 在第一次使用后不能被复制

我们可以看到 WaitGroup 结构体有 2 个成员

  • noCopy

是 go 语言的源码中检测禁止拷贝的技术,如果检测到我们的程序中 WaitGroup 有赋值的操作,那么程序就会报错

  • state1

可以看出 state1 是一个元素个数为 3 个数组,且每个元素都是 占 32 bits

64 位系统里面,64位原子操作需要64位对齐

那么高位的 32 bits 对应的是 counter 计数器,用来表示目前还没有完成任务的协程个数

低 32 bits 对应的是 waiter 的数量,表示目前已经调用了 WaitGroup.Wait 的协程个数

那么剩下的一个 32 bits 就是 sema 信号量的了(后面的源码中会有体现)

func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {

继续看源码

// state returns pointers to the state and sema fields stored within wg.state1.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
  if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
    return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
  } else {
    return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
  }
}

此处我们可以看到 , state 函数是 返回存储在 wg.state1 中的状态和 sema字段 的指针

这里需要重点注意 state() 函数的实现,有 2 种情况

  • 第 1 种 情况是,在 64 位系统下面,返回 sema字段 的指针取的是 &wg.state1[2] ,说明 64 位系统时,state1 数据排布是 : counterwaitersema
  • 第 2 种情况是,32 位系统下面,返回 sema字段 的指针取的是 &wg.state1[0] ,说明 64 位系统时,state1 数据排布是 : semacounterwaiter

具体原因细心的 胖鱼 可能有点想法,

为什么在不同的操作系统里面,数据结构中的 state1 数组数据排布还不一样?

我们仔细看一下上述的源码

64 位系统时:

return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]

32 位系统时

return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]

golang 这样用,主要原因是 golang 把 counter 和 waiter 合并到一起统一看成是 1 个 64位的数据了,因此在不同的操作系统中

由于字节对齐的原因,64位系统时,前面 2 个 32 位数据加起来,正好是 64 位,正好对齐

对于 32 位系统,则是 第 1 个 32 位数据放 sema 更加合适,后面的 2 个 32 位数据就可以统一取出,作为一个 64 位变量

Add 函数主要功能是将 counter +delta ,增加等待协程的个数:

我们可以看到 Add 函数,通过 state 函数获取到 上述 64位的变量(counterwaiter) 和 sema 信号量后,通过 atomic.AddUint64 函数 将 delta 数据 加到 counter 上面

这里为什么是 delta 要左移 32 位呢?

上面我们有说到嘛, state 函数拿出的 64 位变量,高 32 bits 是 counter,低 32 bits 是waiter,此处的 delta 是要加到 counter 上,因此才需要 delta 左移 32 位

func (wg *WaitGroup) Done() {

// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
  wg.Add(-1)
}

Done 函数没有什么特别的,直接上调用 Add 函数来实现的

func (wg *WaitGroup) Wait() {

Wait 函数 主要是增加 waiter 的个数:

阻塞等待 WaitGroup 中couter 的个数变成 0

函数主要是通过 atomic.CompareAndSwapUint64 函数 CAS (比较并且交换)的方式来操作 waiter 的。

很明显该逻辑是 必须要是 true,才能走到里面的实现,进行 runtime_Semacquire(semap) 操作,若是 false ,则需要在循环里面继续再来一次

Waitgroup .go 的具体实现虽然才 141 行 ,里面的具体细节我们还需要反复深究,学习其中的设计原理,例如 state1 结构体成员的设计思想,就非常的巧妙,无需将它拆成 3 个成员,进而无需再操作值的时候加锁,这样性能就得以很好的展现

慢慢的学习好的思想,日拱一卒

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

相关文章
|
1天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
14 4
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
18 4
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
9 2
|
2天前
|
设计模式 Go 调度
Golang深入浅出之-Go语言中的并发模式:Pipeline、Worker Pool等
【5月更文挑战第1天】Go语言并发模拟能力强大,Pipeline和Worker Pool是常用设计模式。Pipeline通过多阶段处理实现高效并行,常见问题包括数据竞争和死锁,可借助通道和`select`避免。Worker Pool控制并发数,防止资源消耗,需注意任务分配不均和goroutine泄露,使用缓冲通道和`sync.WaitGroup`解决。理解和实践这些模式是提升Go并发性能的关键。
14 2
|
2天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
11 2
|
2天前
|
Go
Golang深入浅出之-Go语言代码质量与规范:遵循Gofmt与Linting
【5月更文挑战第1天】本文讨论了如何使用`gofmt`和Lint工具提升Go代码质量。`gofmt`负责自动格式化代码,保持风格统一,而Lint工具如`golint`、`govet`、`staticcheck`则进行静态分析,检查潜在错误和未使用的变量。通过集成`gofmt`检查到CI/CD流程,避免格式冲突,并使用Lint工具发现并修复问题,如未处理的错误、不规范命名。遵循这些最佳实践,可提高代码可读性、团队协作效率和可维护性。
9 3
|
2天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
Go
一个golang并行库源码解析
## 场景 有这样一种场景:四个任务A、B、C, D,其中任务B和C需要并发执行,得到结果1, 任务A执行得到结果2, 结果1和2作为任务D的参数传入,然后执行任务D得到最终结果。我们可以将任务执行顺序用如下图标识: ``` jobA jobB jobC \ \ / \ \ / \ middle \ /
2109 0
|
3天前
|
SQL 安全 Go
【Go语言专栏】Go语言中的安全审计与漏洞修复
【4月更文挑战第30天】本文介绍了Go语言中的安全审计和漏洞修复实践。安全审计包括代码审查、静态分析、运行时分析、渗透测试和专业服务,借助工具如`go vet`、`staticcheck`、`gosec`等。修复漏洞的方法涉及防止SQL注入、XSS攻击、CSRF、不安全反序列化等。遵循最小权限原则、输入验证等最佳实践,结合持续学习,可提升Go应用安全性。参考[Go安全工作组](https://github.com/golang/security)和[OWASP Top 10](https://owasp.org/www-project-top-ten/)深入学习。
|
3天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
14 2