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

WaitGroup使用大家都会,但是其中是怎么实现的我们也需要知道,这样才能在项目中尽可能的避免由于不正确的使用引发的panic。并且本文也将写一下内存对齐方面做一个解析,喜欢大家喜欢。

WaitGroup介绍

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方法里面为什么要这么做之前,我们先来看看内存对齐。

内存对齐

在维基百科https://en.wikipedia.org/wiki/Data_structure_alignment上我们可以看到对于内存对齐的定义:

A memory address a is said to be n-byte aligned when a is a multiple of n bytes (where n is a power of 2).

简而言之,现在的CPU访问内存的时候是一次性访问多个bytes,比如32位架构一次访问4bytes,该处理器只能从地址为4的倍数的内存开始读取数据,所以要求数据在存放的时候首地址的值是4的倍数存放,者就是所谓的内存对齐。

由于找不到Go语言的对齐规则,我对照了一下C语言的内存对齐的规则,可以和Go语言匹配的上,所以先参照下面的规则。

内存对齐遵循下面三个原则:

结构体变量的起始地址能够被其最宽的成员大小整除;

结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节;

结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节;

通过下面的例子来实操一下内存对齐:

在32位架构中,int8占1byte,int32占4bytes,int16占2bytes。

type A struct { a int8 b int32 c int16 } type B struct { a int8 c int16 b int32 } func main() { fmt.Printf("arrange fields to reduce size:\n"+ "A align: %d, size: %d\n" , unsafe.Alignof(A{}), unsafe.Sizeof(A{}) ) fmt.Printf("arrange fields to reduce size:\n"+ "B align: %d, size: %d\n" , unsafe.Alignof(B{}), unsafe.Sizeof(B{}) ) } //output: //arrange fields to reduce size: //A align: 4, size: 12 //arrange fields to reduce size: //B align: 4, size: 8

下面以在32位的架构中运行为例子:

在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则要紧凑些:

Group 50

WaitGroup中state方法的内存对齐

在讲之前需要注意的是noCopy是一个空的结构体,大小为0,不需要做内存对齐,所以大家在看的时候可以忽略这个字段。

在WaitGroup里面,使用了uint32的数组来构造state1字段,然后根据系统的位数的不同构造不同的返回值,下面我面先来说说怎么通过sate1这个字段构建waiter数、计数值、信号量的。

首先unsafe.Pointer来获取state1的地址值然后转换成uintptr类型的,然后判断一下这个地址值是否能被8整除,这里通过地址 mod 8的方式来判断地址是否是64位对齐。

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

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