<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go中由WaitGroup引發(fā)對內(nèi)存對齊思考

          共 6973字,需瀏覽 14分鐘

           ·

          2022-05-31 19:05

          本文使用的go的源碼是1.14.4

          WaitGroup介紹

          WaitGroup 提供了三個方法:

          ????func?(wg?*WaitGroup)?Add(delta?int)
          ????func?(wg?*WaitGroup)?Done()
          ????func?(wg?*WaitGroup)?Wait()
          • Add,用來設(shè)置 WaitGroup 的計數(shù)值;
          • Done,用來將 WaitGroup 的計數(shù)值減 1,其實就是調(diào)用了 Add(-1);
          • Wait,調(diào)用這個方法的 goroutine 會一直阻塞,直到 WaitGroup 的計數(shù)值變?yōu)?0。

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

          解析

          type?noCopy?struct{}

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

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

          內(nèi)存對齊

          在維基百科https://en.wikipedia.org/wiki/Data_structure_alignment上我們可以看到對于內(nèi)存對齊的定義:

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

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

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

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

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

          通過下面的例子來實操一下內(nèi)存對齊:

          在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)中運行為例子:

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

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

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

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

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

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

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

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

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

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

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

          同理32位架構(gòu)想要原子性的操作8bytes,需要由調(diào)用方保證其數(shù)據(jù)地址是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位字對齊,只能讓變量或開辟的結(jié)構(gòu)體、數(shù)組和切片值中的第一個64位字可以被認為是64位字對齊。但是在使用WaitGroup的時候會有嵌套的情況,不能保證總是讓W(xué)aitGroup存在于結(jié)構(gòu)體的第一個字段上,所以我們需要增加填充使它能對齊64位字。

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

          小結(jié)

          這里小結(jié)一下,因為為了完成上面的這篇內(nèi)容實在是查閱了很多資料,才得出這樣的結(jié)果。所以這里小結(jié)一下,在64位架構(gòu)中,CPU每次操作的字長都是8bytes,編譯器會自動幫我們把結(jié)構(gòu)體的第一個字段的地址初始化成64位對齊的,所以64位架構(gòu)上用state1前兩個元素并成uint64來表示statep,state1最后一個元素表示semap;

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

          如果我說錯了,歡迎來diss我,我覺得我需要學(xué)習(xí)的地方還有很多。

          Add 方法

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

          2. 因為計數(shù)器counter可能為負數(shù),所以int32來獲取計數(shù)器的值,waiter不可能為負數(shù),所以使用uint32來獲取;

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

          4. v大于零或者w等于零直接返回,說明這個時候不需要釋放waiter,所以直接返回;

          5. *statep != state到了這個校驗這里,狀態(tài)只能是waiter大于零并且counter為零。當(dāng)waiter大于零的時候是不允許再調(diào)用add方法,counter為零的時候也不能調(diào)用wait方法,所以這里使用state的值和內(nèi)存的地址值進行比較,查看是否調(diào)用了add或者wait導(dǎo)致state變動,如果有就是非法調(diào)用會引起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. 進入for循環(huán)之后Load statep的值,然后分別獲取counter和counter;
          3. 如果counter已經(jīng)為零了,那么直接返回不需要等待;
          4. counter不為零,那么使用CAS將waiter加1,由于CAS可能失敗,所以for循環(huán)會再次的回到這里進行CAS,直到成功;
          5. 調(diào)用runtime_Semacquire掛起等待喚醒;
          6. *statep != 0喚醒之后statep不為零,表示W(wǎng)aitGroup又被重復(fù)使用,這會panic。需要注意的是waitgroup并不是不讓重用,而是不能在wait方法還沒運行完就開始重用。

          waitgroup使用小結(jié)

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

          • 不能將計數(shù)器設(shè)置為負數(shù),否則會發(fā)生panic;注意有兩種方式會導(dǎo)致計數(shù)器為負數(shù),一是調(diào)用 Add 的時候傳遞一個負數(shù),第二是調(diào)用 Done 方法的次數(shù)過多,超過了 WaitGroup 的計數(shù)值;
          • 在使用 WaitGroup 的時候,一定要等所有的 Add 方法調(diào)用之后再調(diào)用 Wait,否則就可能導(dǎo)致 panic;
          • wait還沒結(jié)束就重用 WaitGroup。WaitGroup是可以重用的,但是需要等上一批的goroutine 都調(diào)用wait完畢后才能繼續(xù)重用WaitGroup;

          總結(jié)

          waitgroup里面的代碼實際上是非常的簡單的,這篇文章主要是由waitgroup引入了內(nèi)存對齊這個概念。由waitgroup帶我們看了在實際的代碼中是如何利用內(nèi)存對齊這個概念的,以及如何在32為操作系統(tǒng)中原子性的操作64位長的字段。

          除了內(nèi)存對齊的概念以外通過源碼我們也了解到了使用waitgroup的時候需要怎么做才是符合規(guī)范的,不會引發(fā)panic。

          Reference

          http://blog.newbmiao.com/2020/02/10/dig101-golang-struct-memory-align.html

          https://gfw.go101.org/article/memory-layout.html

          https://golang.org/pkg/sync/atomic/#pkg-note-BUG

          https://en.wikipedia.org/wiki/Data_structure_alignment

          https://www.zhihu.com/question/27862634



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人做爰www看视频软件 | 久久精品人人伦 | 91豆花视频入口网站 | 欧美国产日韩在线观看 | 福利偷拍观看 |