目录
- WaitGroup介绍
WaitGroup的实现
- Add
- Done
- Wait
WaitGroup介绍
waitGroup
,也是在go语言并发中比拟罕用的语法,所以在这里咱们一起分析 waitGroup 的应用形式及其源码解读。
WaitGroup
也是sync 包下一份子,用来解决工作编排的一个并发原语。它次要解决了并发-期待问题:比方当初有三个goroutine
,别离为goroutineA
,goroutineB
,goroutineC
,而goroutineA
须要期待goroutineB
和goroutineC
这一组goroutine全副执行结束后,才能够执行后续业务逻辑。此时就能够应用 WaitGroup
轻松解决。
在这个场景中,goroutineA
为主goroutine,goroutineB
和goroutineC
为子goroutine。goroutineA
则须要在检查点(checkout point) 期待goroutineB
和goroutineC
全副执行结束,如果在执行工作的goroutine
还没全副实现,那么goroutineA
就会阻塞在检查点,直到所有goroutine
都实现后能力继续执行。
代码实现:
package mainimport ( "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 ExecutegoroutineB ExecutegoroutineB 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办法理论就是计数器减1func (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 」第一工夫浏览。每天分享优质文章、大厂教训、大厂面经,助力面试,是每个程序员值得关注的平台。