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

来源:这里教程网 时间:2026-03-03 16:22:47 作者:

WaitGroup 提供了三个方法:     func (wg *WaitGroup) Add(delta int)     func (wg *WaitGroup) Done()     func (wg *WaitGroup) Wait()   Add,用来设置 WaitGroup 的计数值; Done,用来将 WaitGroup 的计数值减 1,其实就是调用了 Add(-1); Wait,调用这个方法的 goroutine 会一直阻塞,直到 WaitGroup 的计数值变为 0。 例子我就不举了,网上是很多的,下面我们直接进入正题。 解析 type noCopy struct{} type WaitGroup struct {     // 避免复制使用的一个技巧,可以告诉vet工具违反了复制使用的规则 noCopy noCopy // 一个复合值,用来表示waiter数、计数值、信号量 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] } }   这里刚开始,WaitGroup就秀了一把肌肉,让我们看看大牛是怎么写代码的,思考一个原子操作在不同架构平台上是怎么操作的,在看state方法里面为什么要这么做之前,我们先来看看内存对齐。 内存对齐 A memory address a is said to be n-byte aligned when a is a multiple of n  (where n is a power of 2). 简而言之,现在的CPU访问内存的时候是一次性访问多个bytes,比如32位架构一次访问4bytes,该处理器只能从地址为4的倍数的内存开始读取数据,所以要求数据在存放的时候首地址的值是4的倍数存放,者就是所谓的内存对齐。 由于找不到Go语言的对齐规则,我对照了一下C语言的内存对齐的规则,可以和Go语言匹配的上,所以先参照下面的规则。 内存对齐遵循下面三个原则: 结构体变量的起始地址能够被其最宽的成员大小整除; 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节; 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节 32位架构的系统中默认的对齐大小是4bytes。 假设结构体A中a的起始地址为0x0000,能够被最宽的数据成员大小4bytes(int32)整除,所以从0x0000开始存放占用一个字节即0x00000x0001;b是int32,占4bytes,所以要满足条件2,需要在a后面padding3个byte,从0x0004开始;c是int16,占2bytes故从0x0008开始占用两个字节,即0x00080x0009;此时整个结构体占用的空间是0x0000~0x0009占用10个字节,10%4 != 0, 不满足第三个原则,所以需要在后面补充两个字节,即最后内存对齐后占用的空间是0x0000~0x000B,一共12个字节。   同理,相比结构体B则要紧凑些:   WaitGroup中state方法的内存对齐 在讲之前需要注意的是noCopy是一个空的结构体,大小为0,不需要做内存对齐,所以大家在看的时候可以忽略这个字段。 WaitGroup里面,使用了uint32的数组来构造state1字段,然后根据系统的位数的不同构造不同的返回值,下面我面先来说说怎么通过sate1这个字段构建waiter数、计数值、信号量的。 首先unsafe.Pointer来获取state1的地址值然后转换成uintptr类型的,然后判断一下这个地址值是否能被8整除,这里通过地址 mod 8的方式来判断地址是否是64位对齐。 因为有内存对齐的存在,在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位对齐的,否则原子访问会有异常 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。

相关推荐