关于面试:深度解析sync-WaitGroup源码及其实现原理

目录

  • WaitGroup介绍
  • WaitGroup的实现

    • Add
    • Done
    • Wait

WaitGroup介绍

waitGroup ,也是在go语言并发中比拟罕用的语法,所以在这里咱们一起分析 waitGroup 的应用形式及其源码解读。

WaitGroup 也是sync 包下一份子,用来解决工作编排的一个并发原语。它次要解决了并发-期待问题:比方当初有三个goroutine,别离为goroutineAgoroutineBgoroutineC,而goroutineA须要期待goroutineBgoroutineC这一组goroutine全副执行结束后,才能够执行后续业务逻辑。此时就能够应用 WaitGroup 轻松解决。

在这个场景中,goroutineA为主goroutine,goroutineBgoroutineC为子goroutine。goroutineA则须要在检查点(checkout point) 期待goroutineBgoroutineC全副执行结束,如果在执行工作的goroutine还没全副实现,那么goroutineA就会阻塞在检查点,直到所有goroutine都实现后能力继续执行。

代码实现:

package main

import (
  "fmt"
  "sync"
)

func goroutineB(wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Println("goroutineB Execute")
  time.Sleep(time.Second)
}

func goroutineC(wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Println("goroutineC Execute")
  time.Sleep(time.Second)
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go goroutineB(&wg)
  go goroutineC(&wg)
  wg.Wait()
  fmt.Println("goroutineB and goroutineC finished...")
}

运行后果:

goroutineC Execute
goroutineB Execute
goroutineB and goroutineC finished...

上述就是WaitGroup 的简略操作,它的语法也是比较简单,提供了三个办法,如下所示:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • Add:用来设置WaitGroup的计数值(子goroutine的数量)
  • Done:用来将WaitGroup的计数值减1,起始就是调用Add(-1)
  • Wait:调用这个办法的goroutine会始终阻塞,直到WaitGroup的技术值变为0

接下来,咱们进行分析 WaitGroup 的源码实现,让其无处可遁,它源码比拟少,除去正文,也就几十行,对老手来说也是一种不错的抉择。

WaitGroup的实现

首先,咱们看看 WaitGroup 的数据结构,它包含了一个noCopy 的辅助字段,一个具备复合意义的state1字段。

  • noCopy 的辅助字段:次要就是辅助 vet 工具查看是否通过 copy 赋值这个 WaitGroup 实例。我会在前面和你详细分析这个字段
  • state1:具备复合意义的字段,蕴含WaitGroup计数值,阻塞在检查点的主gooutine和信号量
type WaitGroup struct {
    // 防止复制应用的一个技巧,能够通知vet工具违反了复制应用的规定
    noCopy noCopy
    // 64bit(8bytes)的值分成两段,高32bit是计数值,低32bit是waiter的计数
    // 另外32bit是用作信号量的
    // 因为64bit值的原子操作须要64bit对齐,然而32bit编译器不反对,所以数组中的元素在不同的架构中不一样,具体解决看上面的办法
    // 总之,会找到对齐的那64bit作为state,其余的32bit做信号量
    state1 [3]uint32
}


// 失去state的地址和信号量的地址
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
    if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
        // 如果地址是64bit对齐的,数组前两个元素做state,后一个元素做信号量
        return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
    } else {
        // 如果地址是32bit对齐的,数组后两个元素用来做state,它能够用来做64bit的原子操作,第一个元素32bit用来做信号量
        return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
    }
}    

因为对 64 位整数的原子操作要求整数的地址是 64 位对齐的,所以针对 64 位和 32 位环境的 state 字段的组成是不一样的。

在 64 位环境下,state1 的第一个元素是 waiter 数,第二个元素是 WaitGroup 的计数值,第三个元素是信号量。

在 32 位环境下,如果 state1 不是 64 位对齐的地址,那么 state1 的第一个元素是信号量,后两个元素别离是 waiter 数和计数值。

接下里,咱们一一看 Add 办法、 Done 办法、 Wait 办法的实现原理。

Add

Add办法实现思路:

Add办法次要操作的state1字段中计数值局部。当Add办法被调用时,首先会将delta参数值左移32位(计数值在高32位),而后外部通过原子操作将这个值加到计数值上。须要留神的是,delta的取值范畴可正可负,因为调用Done()办法时,外部通过Add(-1)办法实现的。

代码实现如下:

func (wg *WaitGroup) Add(delta int) {
  // statep示意wait数和计数值
  // 低32位示意wait数,高32位示意计数值
   statep, semap := wg.state()
   // uint64(delta)<<32 将delta左移32位
    // 因为高32位示意计数值,所以将delta左移32,减少到技术上
   state := atomic.AddUint64(statep, uint64(delta)<<32)
   // 以后计数值
   v := int32(state >> 32)
   // 阻塞在检查点的wait数
   w := uint32(state)
   if v > 0 || w == 0 {
      return
   }
   
   // 如果计数值v为0并且waiter的数量w不为0,那么state的值就是waiter的数量
    // 将waiter的数量设置为0,因为计数值v也是0,所以它们俩的组合*statep间接设置为0即可。此时须要并唤醒所有的waiter
   *statep = 0
   for ; w != 0; w-- {
      runtime_Semrelease(semap, false, 0)
   }
}

Done

外部就是调用Add(-1)办法,这里就不细讲了。

// Done办法理论就是计数器减1
func (wg *WaitGroup) Done() { 
  wg.Add(-1)
}

Wait

wait实现思路:

一直查看state值。如果其中的计数值为零,则阐明所有的子goroutine已全副执行结束,调用者不用期待,间接返回。如果计数值大于零,阐明此时还有工作没有实现,那么调用者变成期待者,须要退出wait队列,并且阻塞本人。

代码实现如下:

func (wg *WaitGroup) Wait() {
   // statep示意wait数和计数值
   // 低32位示意wait数,高32位示意计数值
   statep, semap := wg.state()
   for {
      state := atomic.LoadUint64(statep)
      // 将state右移32位,示意以后计数值
      v := int32(state >> 32)
      // w示意waiter期待值
      w := uint32(state)
      if v == 0 {
         // 如果以后计数值为零,示意以后子goroutine已全副执行结束,则间接返回
         return
      }
      // 否则应用原子操作将state值加一。
      if atomic.CompareAndSwapUint64(statep, state, state+1) {
         // 阻塞休眠期待
         runtime_Semacquire(semap)
         // 被唤醒,不再阻塞,返回
         return
      }
   }
}

到此,waitGroup的根本应用和实现原理已介绍结束了,置信大家已有不一样的播种,咱们下期见。

文章也会继续更新,能够微信搜寻「 迈莫coding 」第一工夫浏览。每天分享优质文章、大厂教训、大厂面经,助力面试,是每个程序员值得关注的平台。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理