溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊

發(fā)布時(shí)間:2021-10-20 10:34:41 來(lái)源:億速云 閱讀:121 作者:iii 欄目:編程語(yǔ)言

本篇內(nèi)容介紹了“如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

WaitGroup介紹

WaitGroup 提供了三個(gè)方法:

    func (wg *WaitGroup) Add(delta int)
    func (wg *WaitGroup) Done()
    func (wg *WaitGroup) Wait()
  • Add,用來(lái)設(shè)置 WaitGroup 的計(jì)數(shù)值;

  • Done,用來(lái)將 WaitGroup 的計(jì)數(shù)值減 1,其實(shí)就是調(diào)用了 Add(-1);

  • Wait,調(diào)用這個(gè)方法的 goroutine 會(huì)一直阻塞,直到 WaitGroup 的計(jì)數(shù)值變?yōu)?0。

例子我就不舉了,網(wǎng)上是很多的,下面我們直接進(jìn)入正題。

解析

type noCopy struct{}

type WaitGroup struct {
    // 避免復(fù)制使用的一個(gè)技巧,可以告訴vet工具違反了復(fù)制使用的規(guī)則
	noCopy noCopy
	// 一個(gè)復(fù)合值,用來(lái)表示waiter數(shù)、計(jì)數(shù)值、信號(hào)量
	state1 [3]uint32
}
// 獲取state的地址和信號(hào)量的地址
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
	if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
		// 如果地址是64bit對(duì)齊的,數(shù)組前兩個(gè)元素做state,后一個(gè)元素做信號(hào)量
		return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
	} else {
		// 如果地址是32bit對(duì)齊的,數(shù)組后兩個(gè)元素用來(lái)做state,它可以用來(lái)做64bit的原子操作,第一個(gè)元素32bit用來(lái)做信號(hào)量
		return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
	}
}

這里剛開(kāi)始,WaitGroup就秀了一把肌肉,讓我們看看大牛是怎么寫代碼的,思考一個(gè)原子操作在不同架構(gòu)平臺(tái)上是怎么操作的,在看state方法里面為什么要這么做之前,我們先來(lái)看看內(nèi)存對(duì)齊。

內(nèi)存對(duì)齊

我們可以看到對(duì)于內(nèi)存對(duì)齊的定義:

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).

簡(jiǎn)而言之,現(xiàn)在的CPU訪問(wèn)內(nèi)存的時(shí)候是一次性訪問(wèn)多個(gè)bytes,比如32位架構(gòu)一次訪問(wèn)4bytes,該處理器只能從地址為4的倍數(shù)的內(nèi)存開(kāi)始讀取數(shù)據(jù),所以要求數(shù)據(jù)在存放的時(shí)候首地址的值是4的倍數(shù)存放,者就是所謂的內(nèi)存對(duì)齊。

由于找不到Go語(yǔ)言的對(duì)齊規(guī)則,我對(duì)照了一下C語(yǔ)言的內(nèi)存對(duì)齊的規(guī)則,可以和Go語(yǔ)言匹配的上,所以先參照下面的規(guī)則。

內(nèi)存對(duì)齊遵循下面三個(gè)原則:

  1. 結(jié)構(gòu)體變量的起始地址能夠被其最寬的成員大小整除;

  2. 結(jié)構(gòu)體每個(gè)成員相對(duì)于起始地址的偏移能夠被其自身大小整除,如果不能則在前一個(gè)成員后面補(bǔ)充字節(jié);

  3. 結(jié)構(gòu)體總體大小能夠被最寬的成員的大小整除,如不能則在后面補(bǔ)充字節(jié);

通過(guò)下面的例子來(lái)實(shí)操一下內(nèi)存對(duì)齊:

在32位架構(gòu)中,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位的架構(gòu)中運(yùn)行為例子:

在32位架構(gòu)的系統(tǒng)中默認(rèn)的對(duì)齊大小是4bytes。

假設(shè)結(jié)構(gòu)體A中a的起始地址為0x0000,能夠被最寬的數(shù)據(jù)成員大小4bytes(int32)整除,所以從0x0000開(kāi)始存放占用一個(gè)字節(jié)即0x00000x0001;b是int32,占4bytes,所以要滿足條件2,需要在a后面padding3個(gè)byte,從0x0004開(kāi)始;c是int16,占2bytes故從0x0008開(kāi)始占用兩個(gè)字節(jié),即0x00080x0009;此時(shí)整個(gè)結(jié)構(gòu)體占用的空間是0x0000~0x0009占用10個(gè)字節(jié),10%4 != 0, 不滿足第三個(gè)原則,所以需要在后面補(bǔ)充兩個(gè)字節(jié),即最后內(nèi)存對(duì)齊后占用的空間是0x0000~0x000B,一共12個(gè)字節(jié)。

如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊

同理,相比結(jié)構(gòu)體B則要緊湊些:

如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊

WaitGroup中state方法的內(nèi)存對(duì)齊

在講之前需要注意的是noCopy是一個(gè)空的結(jié)構(gòu)體,大小為0,不需要做內(nèi)存對(duì)齊,所以大家在看的時(shí)候可以忽略這個(gè)字段。

在WaitGroup里面,使用了uint32的數(shù)組來(lái)構(gòu)造state1字段,然后根據(jù)系統(tǒng)的位數(shù)的不同構(gòu)造不同的返回值,下面我面先來(lái)說(shuō)說(shuō)怎么通過(guò)sate1這個(gè)字段構(gòu)建waiter數(shù)、計(jì)數(shù)值、信號(hào)量的。

首先unsafe.Pointer來(lái)獲取state1的地址值然后轉(zhuǎn)換成uintptr類型的,然后判斷一下這個(gè)地址值是否能被8整除,這里通過(guò)地址 mod 8的方式來(lái)判斷地址是否是64位對(duì)齊。

因?yàn)橛袃?nèi)存對(duì)齊的存在,在64位架構(gòu)里面WaitGroup結(jié)構(gòu)體state1起始的位置肯定是64位對(duì)齊的,所以在64位架構(gòu)上用state1前兩個(gè)元素并成uint64來(lái)表示statep,state1最后一個(gè)元素表示semap;

那么64位架構(gòu)上面獲取state1的時(shí)候能不能第一個(gè)元素表示semap,后兩個(gè)元素拼成64位返回呢?

答案自然是不可以,因?yàn)閡int32的對(duì)齊保證是4bytes,64位架構(gòu)中一次性處理事務(wù)的一個(gè)固定長(zhǎng)度是8bytes,如果用state1的后兩個(gè)元素表示一個(gè)64位字的字段的話CPU需要讀取內(nèi)存兩次,不能保證原子性。

但是在32位架構(gòu)里面,一個(gè)字長(zhǎng)是4bytes,要操作64位的數(shù)據(jù)分布在兩個(gè)數(shù)據(jù)塊中,需要兩次操作才能完成訪問(wèn)。如果兩次操作中間有可能別其他操作修改,不能保證原子性。

同理32位架構(gòu)想要原子性的操作8bytes,需要由調(diào)用方保證其數(shù)據(jù)地址是64位對(duì)齊的,否則原子訪問(wèn)會(huì)有異常,我們可以看到描述:

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位字對(duì)齊,只能讓變量或開(kāi)辟的結(jié)構(gòu)體、數(shù)組和切片值中的第一個(gè)64位字可以被認(rèn)為是64位字對(duì)齊。但是在使用WaitGroup的時(shí)候會(huì)有嵌套的情況,不能保證總是讓W(xué)aitGroup存在于結(jié)構(gòu)體的第一個(gè)字段上,所以我們需要增加填充使它能對(duì)齊64位字。

在32位架構(gòu)中,WaitGroup在初始化的時(shí)候,分配內(nèi)存地址的時(shí)候是隨機(jī)的,所以WaitGroup結(jié)構(gòu)體state1起始的位置不一定是64位對(duì)齊,可能會(huì)是:uintptr(unsafe.Pointer(&wg.state1))%8 = 4,如果出現(xiàn)這樣的情況,那么就需要用state1的第一個(gè)元素做padding,用state1的后兩個(gè)元素合并成uint64來(lái)表示statep。

小結(jié)

這里小結(jié)一下,因?yàn)闉榱送瓿缮厦娴倪@篇內(nèi)容實(shí)在是查閱了很多資料,才得出這樣的結(jié)果。所以這里小結(jié)一下,在64位架構(gòu)中,CPU每次操作的字長(zhǎng)都是8bytes,編譯器會(huì)自動(dòng)幫我們把結(jié)構(gòu)體的第一個(gè)字段的地址初始化成64位對(duì)齊的,所以64位架構(gòu)上用state1前兩個(gè)元素并成uint64來(lái)表示statep,state1最后一個(gè)元素表示semap;

然后在32位架構(gòu)中,在初始化WaitGroup的時(shí)候,編譯器只能保證32位對(duì)齊,不能保證64位對(duì)齊,所以通過(guò)uintptr(unsafe.Pointer(&wg.state1))%8判斷是否等于0來(lái)看state1內(nèi)存地址是否是64位對(duì)齊,如果是,那么也和64位架構(gòu)一樣,用state1前兩個(gè)元素并成uint64來(lái)表示statep,state1最后一個(gè)元素表示semap,否則用state1的第一個(gè)元素做padding,用state1的后兩個(gè)元素合并成uint64來(lái)表示statep。

如果我說(shuō)錯(cuò)了,歡迎來(lái)diss我,我覺(jué)得我需要學(xué)習(xí)的地方還有很多。

如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊

Add 方法

func (wg *WaitGroup) Add(delta int) {
	// 獲取狀態(tài)值
	statep, semap := wg.state()
	...
	// 高32bit是計(jì)數(shù)值v,所以把delta左移32,增加到計(jì)數(shù)上
	state := atomic.AddUint64(statep, uint64(delta)<<32)
	// 獲取計(jì)數(shù)器的值
	v := int32(state >> 32)
	// 獲取waiter的值
	w := uint32(state)
	...
	// 任務(wù)計(jì)數(shù)器不能為負(fù)數(shù)
	if v < 0 {
		panic("sync: negative WaitGroup counter")
	}
	// wait不等于0說(shuō)明已經(jīng)執(zhí)行了Wait,此時(shí)不容許Add
	if w != 0 && delta > 0 && v == int32(delta) {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
	// 計(jì)數(shù)器的值大于或者沒(méi)有waiter在等待,直接返回
	if v > 0 || w == 0 {
		return
	} 
	if *statep != state {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
	// 此時(shí),counter一定等于0,而waiter一定大于0
	// 先把counter置為0,再釋放waiter個(gè)數(shù)的信號(hào)量
	*statep = 0
	for ; w != 0; w-- {
		//釋放信號(hào)量,執(zhí)行一次釋放一個(gè),喚醒一個(gè)等待者
		runtime_Semrelease(semap, false, 0)
	}
}
  1. add方法首先會(huì)調(diào)用state方法獲取statep、semap的值。statep是一個(gè)uint64類型的值,高32位用來(lái)記錄add方法傳入的delta值之和;低32位用來(lái)表示調(diào)用wait方法等待的goroutine的數(shù)量,也就是waiter的數(shù)量。如下:

如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊

  1. add方法會(huì)調(diào)用atomic.AddUint64方法將傳入的delta左移32位,也就是將counter加上delta的值;

  2. 因?yàn)橛?jì)數(shù)器counter可能為負(fù)數(shù),所以int32來(lái)獲取計(jì)數(shù)器的值,waiter不可能為負(fù)數(shù),所以使用uint32來(lái)獲??;

  3. 接下來(lái)就是一系列的校驗(yàn),v不能小于零表示任務(wù)計(jì)數(shù)器不能為負(fù)數(shù),否則會(huì)panic;w不等于,并且v的值等于delta表示wait方法先于add方法執(zhí)行,此時(shí)也會(huì)panic,因?yàn)閣aitgroup不允許調(diào)用了Wait方法后還調(diào)用add方法;

  4. v大于零或者w等于零直接返回,說(shuō)明這個(gè)時(shí)候不需要釋放waiter,所以直接返回;

  5. *statep != state到了這個(gè)校驗(yàn)這里,狀態(tài)只能是waiter大于零并且counter為零。當(dāng)waiter大于零的時(shí)候是不允許再調(diào)用add方法,counter為零的時(shí)候也不能調(diào)用wait方法,所以這里使用state的值和內(nèi)存的地址值進(jìn)行比較,查看是否調(diào)用了add或者wait導(dǎo)致state變動(dòng),如果有就是非法調(diào)用會(huì)引起panic;

  6. 最后將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不為零,表示W(wǎng)aitGroup又被重復(fù)使用,這回panic
			if *statep != 0 {
				panic("sync: WaitGroup is reused before previous Wait has returned")
			}
			...
         	// 直接返回   
			return
		}
	}
}
  1. Wait方法首先也是調(diào)用state方法獲取狀態(tài)值;

  2. 進(jìn)入for循環(huán)之后Load statep的值,然后分別獲取counter和counter;

  3. 如果counter已經(jīng)為零了,那么直接返回不需要等待;

  4. counter不為零,那么使用CAS將waiter加1,由于CAS可能失敗,所以for循環(huán)會(huì)再次的回到這里進(jìn)行CAS,直到成功;

  5. 調(diào)用runtime_Semacquire掛起等待喚醒;

  6. *statep != 0喚醒之后statep不為零,表示W(wǎng)aitGroup又被重復(fù)使用,這會(huì)panic。需要注意的是waitgroup并不是不讓重用,而是不能在wait方法還沒(méi)運(yùn)行完就開(kāi)始重用。

waitgroup使用小結(jié)

看完了waitgroup的add方法與wait方法,我們發(fā)現(xiàn)里面有很多校驗(yàn),使用不當(dāng)會(huì)導(dǎo)致panic,所以我們需要總結(jié)一下如何正確使用:

  • 不能將計(jì)數(shù)器設(shè)置為負(fù)數(shù),否則會(huì)發(fā)生panic;注意有兩種方式會(huì)導(dǎo)致計(jì)數(shù)器為負(fù)數(shù),一是調(diào)用 Add 的時(shí)候傳遞一個(gè)負(fù)數(shù),第二是調(diào)用 Done 方法的次數(shù)過(guò)多,超過(guò)了 WaitGroup 的計(jì)數(shù)值;

  • 在使用 WaitGroup 的時(shí)候,一定要等所有的 Add 方法調(diào)用之后再調(diào)用 Wait,否則就可能導(dǎo)致 panic;

  • wait還沒(méi)結(jié)束就重用 WaitGroup。WaitGroup是可以重用的,但是需要等上一批的goroutine 都調(diào)用wait完畢后才能繼續(xù)重用WaitGroup;

“如何理解Go中由WaitGroup引發(fā)對(duì)內(nèi)存對(duì)齊”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI