您好,登錄后才能下訂單哦!
本篇內容介紹了“如何理解Go中由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方法里面為什么要這么做之前,我們先來看看內存對齊。
我們可以看到對于內存對齊的定義:
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則要緊湊些:
在講之前需要注意的是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。
這里小結一下,因為為了完成上面的這篇內容實在是查閱了很多資料,才得出這樣的結果。所以這里小結一下,在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我,我覺得我需要學習的地方還有很多。
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,所以直接返回;
*statep != state
到了這個校驗這里,狀態只能是waiter大于零并且counter為零。當waiter大于零的時候是不允許再調用add方法,counter為零的時候也不能調用wait方法,所以這里使用state的值和內存的地址值進行比較,查看是否調用了add或者wait導致state變動,如果有就是非法調用會引起panic;
最后將statep值重置為零,然后釋放所有的waiter;
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的add方法與wait方法,我們發現里面有很多校驗,使用不當會導致panic,所以我們需要總結一下如何正確使用:
不能將計數器設置為負數,否則會發生panic;注意有兩種方式會導致計數器為負數,一是調用 Add 的時候傳遞一個負數,第二是調用 Done 方法的次數過多,超過了 WaitGroup 的計數值;
在使用 WaitGroup 的時候,一定要等所有的 Add 方法調用之后再調用 Wait,否則就可能導致 panic;
wait還沒結束就重用 WaitGroup。WaitGroup是可以重用的,但是需要等上一批的goroutine 都調用wait完畢后才能繼續重用WaitGroup;
“如何理解Go中由WaitGroup引發對內存對齊”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。