*statep != state到了这个校验这里,状态只能是waiter大于零并且counter为零。当waiter大于零的时候是不允许再调用add方法,counter为零的时候也不能调用wait方法,所以这里使用state的值和内存的地址值进行比较,查看是否调用了add或者wait导致state变动,如果有就是非法调用会引起panic;
最后将statep值重置为零,然后释放所有的waiter;
Wait方法 func (wg *WaitGroup) Wait() { statep, semap := wg.state() ... for { state := atomic.LoadUint64(statep) // 获取counter v := int32(state >> 32) // 获取waiter w := uint32(state) // counter为零,不需要等待直接返回 if v == 0 { ... return } // 使用CAS将waiter加1 if atomic.CompareAndSwapUint64(statep, state, state+1) { ... // 挂起等待唤醒 runtime_Semacquire(semap) // 唤醒之后statep不为零,表示WaitGroup又被重复使用,这回panic if *statep != 0 { panic("sync: WaitGroup is reused before previous Wait has returned") } ... // 直接返回 return } } }Wait方法首先也是调用state方法获取状态值;
进入for循环之后Load statep的值,然后分别获取counter和counter;
如果counter已经为零了,那么直接返回不需要等待;
counter不为零,那么使用CAS将waiter加1,由于CAS可能失败,所以for循环会再次的回到这里进行CAS,直到成功;
调用runtime_Semacquire挂起等待唤醒;
*statep != 0唤醒之后statep不为零,表示WaitGroup又被重复使用,这会panic。需要注意的是waitgroup并不是不让重用,而是不能在wait方法还没运行完就开始重用。
waitgroup使用小结看完了waitgroup的add方法与wait方法,我们发现里面有很多校验,使用不当会导致panic,所以我们需要总结一下如何正确使用:
不能将计数器设置为负数,否则会发生panic;注意有两种方式会导致计数器为负数,一是调用 Add 的时候传递一个负数,第二是调用 Done 方法的次数过多,超过了 WaitGroup 的计数值;
在使用 WaitGroup 的时候,一定要等所有的 Add 方法调用之后再调用 Wait,否则就可能导致 panic;
wait还没结束就重用 WaitGroup。WaitGroup是可以重用的,但是需要等上一批的goroutine 都调用wait完毕后才能继续重用WaitGroup;
总结waitgroup里面的代码实际上是非常的简单的,这篇文章主要是由waitgroup引入了内存对齐这个概念。由waitgroup带我们看了在实际的代码中是如何利用内存对齐这个概念的,以及如何在32为操作系统中原子性的操作64位长的字段。
除了内存对齐的概念以外通过源码我们也了解到了使用waitgroup的时候需要怎么做才是符合规范的,不会引发panic。
Referencehttps://gfw.go101.org/article/memory-layout.html
https://en.wikipedia.org/wiki/Data_structure_alignment
https://www.zhihu.com/question/27862634