Go中由WaitGroup引发对内存对齐思考 (2)

因为有内存对齐的存在,在64位架构里面WaitGroup结构体state1起始的位置肯定是64位对齐的,所以在64位架构上用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap;

那么64位架构上面获取state1的时候能不能第一个元素表示semap,后两个元素拼成64位返回呢?

答案自然是不可以,因为uint32的对齐保证是4bytes,64位架构中一次性处理事务的一个固定长度是8bytes,如果用state1的后两个元素表示一个64位字的字段的话CPU需要读取内存两次,不能保证原子性。

但是在32位架构里面,一个字长是4bytes,要操作64位的数据分布在两个数据块中,需要两次操作才能完成访问。如果两次操作中间有可能别其他操作修改,不能保证原子性。

同理32位架构想要原子性的操作8bytes,需要由调用方保证其数据地址是64位对齐的,否则原子访问会有异常,我们在这里https://golang.org/pkg/sync/atomic/#pkg-note-BUG可以看到描述:

On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

所以为了保证64位字对齐,只能让变量或开辟的结构体、数组和切片值中的第一个64位字可以被认为是64位字对齐。但是在使用WaitGroup的时候会有嵌套的情况,不能保证总是让WaitGroup存在于结构体的第一个字段上,所以我们需要增加填充使它能对齐64位字。

在32位架构中,WaitGroup在初始化的时候,分配内存地址的时候是随机的,所以WaitGroup结构体state1起始的位置不一定是64位对齐,可能会是:uintptr(unsafe.Pointer(&wg.state1))%8 = 4,如果出现这样的情况,那么就需要用state1的第一个元素做padding,用state1的后两个元素合并成uint64来表示statep。

小结

这里小结一下,因为为了完成上面的这篇内容实在是查阅了很多资料,才得出这样的结果。所以这里小结一下,在64位架构中,CPU每次操作的字长都是8bytes,编译器会自动帮我们把结构体的第一个字段的地址初始化成64位对齐的,所以64位架构上用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap;

然后在32位架构中,在初始化WaitGroup的时候,编译器只能保证32位对齐,不能保证64位对齐,所以通过uintptr(unsafe.Pointer(&wg.state1))%8判断是否等于0来看state1内存地址是否是64位对齐,如果是,那么也和64位架构一样,用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap,否则用state1的第一个元素做padding,用state1的后两个元素合并成uint64来表示statep。

如果我说错了,欢迎来diss我,我觉得我需要学习的地方还有很多。

Add 方法 func (wg *WaitGroup) Add(delta int) { // 获取状态值 statep, semap := wg.state() ... // 高32bit是计数值v,所以把delta左移32,增加到计数上 state := atomic.AddUint64(statep, uint64(delta)<<32) // 获取计数器的值 v := int32(state >> 32) // 获取waiter的值 w := uint32(state) ... // 任务计数器不能为负数 if v < 0 { panic("sync: negative WaitGroup counter") } // wait不等于0说明已经执行了Wait,此时不容许Add if w != 0 && delta > 0 && v == int32(delta) { panic("sync: WaitGroup misuse: Add called concurrently with Wait") } // 计数器的值大于或者没有waiter在等待,直接返回 if v > 0 || w == 0 { return } if *statep != state { panic("sync: WaitGroup misuse: Add called concurrently with Wait") } // 此时,counter一定等于0,而waiter一定大于0 // 先把counter置为0,再释放waiter个数的信号量 *statep = 0 for ; w != 0; w-- { //释放信号量,执行一次释放一个,唤醒一个等待者 runtime_Semrelease(semap, false, 0) } }

add方法首先会调用state方法获取statep、semap的值。statep是一个uint64类型的值,高32位用来记录add方法传入的delta值之和;低32位用来表示调用wait方法等待的goroutine的数量,也就是waiter的数量。如下:

add方法会调用atomic.AddUint64方法将传入的delta左移32位,也就是将counter加上delta的值;

因为计数器counter可能为负数,所以int32来获取计数器的值,waiter不可能为负数,所以使用uint32来获取;

接下来就是一系列的校验,v不能小于零表示任务计数器不能为负数,否则会panic;w不等于,并且v的值等于delta表示wait方法先于add方法执行,此时也会panic,因为waitgroup不允许调用了Wait方法后还调用add方法;

v大于零或者w等于零直接返回,说明这个时候不需要释放waiter,所以直接返回;

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpyxgp.html