<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 并發(fā)編程 — sync.Pool 源碼級(jí)原理剖析 [2] 終結(jié)篇

          共 14088字,需瀏覽 29分鐘

           ·

          2021-03-03 09:02

          大綱

          • 前情提要

          • 原理剖析

            • 數(shù)據(jù)結(jié)構(gòu)

            • Get

            • Put

            • runtime

          • 思考問題

            • 1. 如果不是 Pool.Get 申請(qǐng)的對(duì)象,調(diào)用了 Put ,會(huì)怎么樣?

            • 2. `Pool.Get` 出來的對(duì)象,為什么要 `Pool.Put` 放回 Pool 池,是為了不變成自己討厭的垃圾嗎?

            • 3. Pool 本身允許復(fù)制之后使用嗎?

          • 總結(jié)


          前情提要


          上次我們從使用層面做了梳理分析,詳情見: Go 并發(fā)編程—深入淺出sync.Pool [1] 使用姿勢(shì)篇,得到以下幾點(diǎn)小知識(shí):

          1. sync.Pool 本質(zhì)用途是增加臨時(shí)對(duì)象的重用率,減少 GC 負(fù)擔(dān);
          2. 不能對(duì) Pool.Get 出來的對(duì)象做預(yù)判,有可能是新的(新分配的),有可能是舊的(之前人用過,然后 Put 進(jìn)去的);
          3. 不能對(duì) Pool 池里的元素個(gè)數(shù)做假定,你不能夠;
          4. sync.Pool 本身的 Get, Put 調(diào)用是并發(fā)安全的,sync.New 指向的初始化函數(shù)會(huì)并發(fā)調(diào)用,里面安不安全只有自己知道;
          5. 當(dāng)用完一個(gè)從 Pool 取出的實(shí)例時(shí)候,一定要記得調(diào)用 Put,否則 Pool 無法復(fù)用這個(gè)實(shí)例,通常這個(gè)用 defer 完成;

          官方開頭聲明:

          A Pool is a set of temporary objects that may be individually saved and retrieved.

          并且還制作了一個(gè)演示動(dòng)畫視頻來幫助理解,詳情見: Go 并發(fā)編程 — 有趣的sync.Pool原理動(dòng)畫。


          本篇是 sync.Pool 源碼級(jí)別的分析,屬于 sync.Pool 分析完結(jié)篇,三次分享梳理循序漸進(jìn),配合一起學(xué)習(xí)效果更好哦。


          原理剖析


          下面我們從數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)邏輯來深入剖析下 sync.Pool 的原理。

          數(shù)據(jù)結(jié)構(gòu)

          Pool 結(jié)構(gòu)

          sturct Pool  結(jié)構(gòu)是給到用戶的使用的結(jié)構(gòu),定義:

          type Pool struct {
              // 用于檢測(cè) Pool 池是否被 copy,因?yàn)?Pool 不希望被 copy;
              // 有了這個(gè)字段之后,可用用 go vet 工具檢測(cè),在編譯期間就發(fā)現(xiàn)問題;
              noCopy noCopy   
              
              // 數(shù)組結(jié)構(gòu),對(duì)應(yīng)每個(gè) P,數(shù)量和 P 的數(shù)量一致;
              local     unsafe.Pointer 
              localSize uintptr        

              // GC 到時(shí),victim 和 victimSize 會(huì)分別接管 local 和 localSize;
              // victim 的目的是為了減少 GC 后冷啟動(dòng)導(dǎo)致的性能抖動(dòng),讓分配對(duì)象更平滑;
              victim     unsafe.Pointer 
              victimSize uintptr      

              // 對(duì)象初始化構(gòu)造方法,使用方定義
              New func() interface{}
          }

          有幾個(gè)注意點(diǎn):

          1. noCopy 為了防止 copy 加的打樁代碼,但這個(gè)阻止不了編譯,只能通過 go vet 檢查出來;
          2. locallocalSize 這兩個(gè)字段實(shí)現(xiàn)了一個(gè)數(shù)組,數(shù)組元素為 poolLocal 結(jié)構(gòu),用來管理臨時(shí)對(duì)象;
          3. victimvictimSize  這個(gè)是在 poolCleanup 流程里賦值了,賦值的內(nèi)容就是 local  和 localSize 。victim 機(jī)制是把 Pool 池的清理由一輪 GC 改成 兩輪 GC,進(jìn)而提高對(duì)象的復(fù)用率,減少抖動(dòng);
          4. 使用方只能賦值 New 字段,定義對(duì)象初始化構(gòu)造行為;

          poolLocal 結(jié)構(gòu)

          該結(jié)構(gòu)是管理 Pool 池里 cache 元素的關(guān)鍵結(jié)構(gòu),Pool.local  指向的就是這么一個(gè)類型的數(shù)組,這個(gè)結(jié)構(gòu)值得注意的一點(diǎn)是使用了內(nèi)存填充,對(duì)齊 cache line,防止 false sharing 性能問題的技巧。

          Pool 里面該結(jié)構(gòu)數(shù)組是按照 P 的個(gè)數(shù)分配的,每個(gè) P 都對(duì)應(yīng)一個(gè)這個(gè)結(jié)構(gòu)。

          // Pool.local 指向的數(shù)組元素類型
          type poolLocal struct {
              poolLocalInternal

              // 把 poolLocal 填充至 128 字節(jié)對(duì)齊,避免 false sharing 引起的性能問題
              pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
          }

          // 管理 cache 的內(nèi)部結(jié)構(gòu),跟每個(gè) P 對(duì)應(yīng),操作無需加鎖
          type poolLocalInternal struct {
              // 每個(gè) P 的私有,使用時(shí)無需加鎖
              private interface{}
              // 雙鏈表結(jié)構(gòu),用于掛接 cache 元素
              shared  poolChain
          }

          poolChain

          我們可以稍微看下 poolChain 結(jié)構(gòu),這個(gè)純粹是一個(gè)連接件,本身空間也就兩指針,占用內(nèi)存 16 Byte。

          type poolChain struct {
              head *poolChainElt
              tail *poolChainElt
          }

          所以關(guān)鍵還是鏈表的元素,鏈表元素的結(jié)構(gòu)是 poolChainElt,這個(gè)結(jié)構(gòu)體長(zhǎng)這樣:

          type poolChainElt struct {
              // 本質(zhì)是個(gè)數(shù)組內(nèi)存空間,管理成 ringbuffer 的模式;
              poolDequeue

              // 鏈表指針
              next, prev *poolChainElt
          }

          type poolDequeue struct {
              headTail uint64

              // vals is a ring buffer of interface{} values stored in this
              // dequeue. The size of this must be a power of 2.
              vals []eface
          }

          poolChainElt 是雙鏈表的元素點(diǎn),里面其實(shí)是一段數(shù)組空間,類似于 ringbuffer,Pool 管理的 cache 對(duì)象就都存儲(chǔ)在 poolDequeue 的 vals[] 數(shù)組里。


          Get

          func (p *Pool) Get() interface{} {
              // 把 G 鎖住在當(dāng)前 M(聲明當(dāng)前 M 不能被搶占),返回 M 綁定的 P 的 ID
              // 在當(dāng)前的場(chǎng)景,也可以認(rèn)為是 G 綁定到 P,因?yàn)檫@種場(chǎng)景 P 不可能被搶占,只有系統(tǒng)調(diào)用的時(shí)候才有 P 被搶占的場(chǎng)景;
              l, pid := p.pin()
              // 如果能從 private 取出緩存的元素,那么將是最快的路徑;
              x := l.private
              l.private = nil
              if x == nil {
                  // 從 shared 隊(duì)列里獲取,shared 隊(duì)列在 Get 獲取,在 Put 投遞;
                  x, _ = l.shared.popHead()
                  if x == nil {
                      // 嘗試從獲取其他 P 的隊(duì)列里取元素,或者嘗試從 victim cache 里取元素
                      x = p.getSlow(pid)
                  }
              }
              // G-M 鎖定解除
              runtime_procUnpin()

              // 最慢的路徑:現(xiàn)場(chǎng)初始化,這種場(chǎng)景是 Pool 池里一個(gè)對(duì)象都沒有,只能現(xiàn)場(chǎng)創(chuàng)建;
              if x == nil && p.New != nil {
                  x = p.New()
              }
              // 返回對(duì)象
              return x
          }

          Get 的語義就是從 Pool 池里取一個(gè)元素出來,這里的重點(diǎn)是:元素是層層 cache 的,由最快到最慢一層層嘗試。最快的是本 P 對(duì)應(yīng)的列表里通過 private  字段直接取出,最慢的就是調(diào)用 New 函數(shù)現(xiàn)場(chǎng)構(gòu)造。

          嘗試路徑:

          1. 當(dāng)前 P 對(duì)應(yīng)的 local.private 字段;
          2. 當(dāng)前 P 對(duì)應(yīng)的 local 的雙向鏈表;
          3. 其他 P 對(duì)應(yīng)的 local 列表;
          4. victim cache 里的元素;
          5. New 現(xiàn)場(chǎng)構(gòu)造;

          runtime_procPin

          runtime_procPinprocPin  的一層封裝,procPin 實(shí)現(xiàn)如下:

          func procPin() int {
              _g_ := getg()
              mp := _g_.m

              mp.locks++
              return int(mp.p.ptr().id)
          }

          procPin 函數(shù)的目的是為了當(dāng)前 G 被搶占了執(zhí)行權(quán)限(也就是說,當(dāng)前 G 就在當(dāng)前 M 上不走了),這里的核心實(shí)現(xiàn)是對(duì) mp.locks++ 操作,在 newstack  里會(huì)對(duì)此條件做判斷,如果

          if preempt {
              // 已經(jīng)打了搶占標(biāo)識(shí)了,但是還需要判斷條件滿足才能讓出執(zhí)行權(quán);
              if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
                  gp.stackguard0 = gp.stack.lo + _StackGuard
                  gogo(&gp.sched) // never return
              }
          }

          Pool.pinSlow

          這個(gè)函數(shù)必須提一下,這個(gè)函數(shù)做了非常重要的事情,一般是 Pool 第一次調(diào)用 Get 的時(shí)候才會(huì)走進(jìn)來(注意,是每個(gè) P 的第一次 Get 調(diào)用,但是只有一個(gè) P 上的 G 才能干成事,因?yàn)橛?allPoolsMu 鎖互斥)。

          func (p *Pool) pinSlow() (*poolLocal, int) {
              // G-M 先解鎖
              runtime_procUnpin()
              // 以下邏輯在全局鎖 allPoolsMu 內(nèi) 
              allPoolsMu.Lock()
              defer allPoolsMu.Unlock()
              // 獲取當(dāng)前 G-M-P ,P 的 id
              pid := runtime_procPin()
              s := p.localSize
              l := p.local
              if uintptr(pid) < s {
                  return indexLocal(l, pid), pid
              }
              if p.local == nil {
                  // 首次,Pool 需要把自己注冊(cè)進(jìn) allPools 數(shù)組
                  allPools = append(allPools, p)
              }
              // P 的個(gè)數(shù)
              size := runtime.GOMAXPROCS(0)
              // local 數(shù)組的大小就等于 runtime.GOMAXPROCS(0)
              local := make([]poolLocal, size)
              atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
              atomic.StoreUintptr(&p.localSize, uintptr(size))         // store-release
              return &local[pid], pid
          }

          pinSlow 主要做以下幾個(gè)事情:

          1. 首次 Pool 需要把自己注冊(cè)進(jìn) allPools 數(shù)組;
          2. Pool.local 數(shù)組按照 runtime.GOMAXPROCS(0) 的大小進(jìn)行分配,如果是默認(rèn)的,那么這個(gè)就是 P 的個(gè)數(shù),也就是 CPU 的個(gè)數(shù);

          runtime_procUnpin

          這個(gè)是對(duì)應(yīng) runtime_procPin 配套的函數(shù),聲明該 M 可以被搶占,字段 m.locks-- 。

          func procUnpin() {
              _g_ := getg()
              _g_.m.locks--
          }


          Put

          Put 方法非常簡(jiǎn)單,因?yàn)槭呛笾锰幚?,該做的都在前面做好了,而清理?dòng)作又是在 runtime 的后臺(tái)流程·,所以這里只是把元素放置到隊(duì)列里就完成了。

          // Put 一個(gè)元素進(jìn)池子;
          func (p *Pool) Put(x interface{}) {
              if x == nil {
                  return
              }
              // G-M 鎖定
              l, _ := p.pin()
              if l.private == nil {
                  // 嘗試放到最快的位置,這個(gè)位置也跟 Get 請(qǐng)求的順序是一一對(duì)應(yīng)的;
                  l.private = x
                  x = nil
              }
              if x != nil {
                  // 放到雙向鏈表中
                  l.shared.pushHead(x)
              }
              // G-M 鎖定解除
              runtime_procUnpin()
          }

          但是也要注意一個(gè)小點(diǎn),就是 Put 也會(huì)調(diào)用 p.pin() ,所以 Pool.local 也可能會(huì)在這里創(chuàng)建。


          runtime

          全局變量

          每一個(gè) Pool 結(jié)構(gòu)都加到了全局隊(duì)列里,在 src/sync/pool.go 文件里,定義了幾個(gè)全局變量:

          var (
              // 互斥用
              allPoolsMu Mutex

              // 全局的 Pool 數(shù)組,所有的 Pool 都在這里有注冊(cè)地址;
              allPools []*Pool

              // 配合 victim 機(jī)制用的;
              oldPools []*Pool
          )

          后臺(tái)流程

          init

          初始化的時(shí)候注冊(cè)清理函數(shù)。

          func init() {
              runtime_registerPoolCleanup(poolCleanup)
          }

          在 Golang GC 開始的時(shí)候 gcStart 調(diào)用 clearpools() 函數(shù)就會(huì)調(diào)用到 poolCleanup 函數(shù)。也就是說,每一輪 GC 都是對(duì)所有的 Pool 做一次清理。

          poolCleanup

          這個(gè)是定期執(zhí)行的,在 sync package init 的時(shí)候注冊(cè),由 runtime 后臺(tái)執(zhí)行,內(nèi)容就是批量清理 allPools 里的元素。

          func poolCleanup() {
              // 清理 oldPools 上的 victim 的元素
              for _, p := range oldPools {
                  p.victim = nil
                  p.victimSize = 0
              }

              // 把 local cache 遷移到 victim 上;
              // 這樣就不致于讓 GC 把所有的 Pool 都清空了,有 victim 再兜底以下,這樣可以防止抖動(dòng);
              for _, p := range allPools {
                  p.victim = p.local
                  p.victimSize = p.localSize
                  p.local = nil
                  p.localSize = 0
              }

              // 清理一波所有的 allPools
              oldPools, allPools = allPools, nil
          }

          victim 把回收動(dòng)作由一次變?yōu)榱藘纱?,這樣更抗造一點(diǎn)。每次清理都是只有上次 cache 的對(duì)象才會(huì)被真正清理掉,當(dāng)前的 cache 對(duì)象只是移到回收站(victim)。

          知識(shí)小結(jié)

          1. 每輪 GC 開始都會(huì)調(diào)用 poolCleanup 函數(shù);
          2. 使用兩輪清理過程來抵抗波動(dòng),也就是 local cache 和 victim cache 配合;

          思考問題


          原理上面已經(jīng)剖析的非常清晰了,現(xiàn)在我們思考一些與眾不同的問題:


          1. 如果不是 Pool.Get 申請(qǐng)的對(duì)象,調(diào)用了 Put ,會(huì)怎么樣?

          不會(huì)有任何異常(是不是驚呆了),Pool 池里能接納任意來源,任意類型的對(duì)象。就算不是 Pool.Get 出來的對(duì)象,也能正常調(diào)用 Pool.Put,而一旦你做了這個(gè)事情之后,Pool 池里的就不是單一的對(duì)象元素了,而是一個(gè)雜貨鋪了。

          原因解析

          1. 首先,Put(x interface{})  接口沒有對(duì) x 類型做判斷和斷言;
          2. 其次,Pool.Put 內(nèi)部也沒有對(duì)類型做斷言和判斷,無法追究元素是否是來自于 Get 的接口;

          所以,在上一篇剖析 Pool 使用姿勢(shì)文章的中,在調(diào)用 Pool.Get 出來元素之后,我有一行類型斷言就是這個(gè)意思:

          buffer := bufferPool.Get()
          _ = buffer.(*[]byte)

          注意這個(gè)很重要,因?yàn)?sync.Pool 框架支持存放任何類型,本質(zhì)上可以是一個(gè)雜貨鋪,所以 Get 出來和 Put 進(jìn)去的對(duì)象類型要業(yè)務(wù)自己把控。


          2. Pool.Get 出來的對(duì)象,為什么要 Pool.Put 放回 Pool 池,是為了不變成自己討厭的垃圾嗎?

          首先,從使用姿勢(shì)來說,Pool.GetPool.Put 一定要配套使用,通常使用 defer Pool.Put  這種形式保證釋放元素進(jìn)池子。

          你想過建議 Get,Put 配套使用的原因嗎?如果不配套是會(huì)變成不可回收的垃圾嗎?

          首先,這個(gè)說法是錯(cuò)誤的,雖然 Pool.GetPool.Put 通常是配套使用的,但是也絕對(duì)不是硬性要求,Get.Put 出來的元素使用完之后,就算不調(diào)用 Pool.Put  放進(jìn)池子也不會(huì)成為垃圾,而是自然再?zèng)]有人用到這個(gè)對(duì)象的時(shí)候,GC 會(huì)釋放他。

          舉個(gè)極限的例子,如果我使用 Pool 的姿勢(shì)上做下改動(dòng),每次都 Pool.Get ,一次都不調(diào)用 Pool.Put ,那么會(huì)有什么情況發(fā)生?

          答案是:沒啥情況發(fā)生,程序照常運(yùn)行。只不過 Pool 每次 Get 的時(shí)候,都要執(zhí)行 New 函數(shù)來構(gòu)造對(duì)象而已,Pool 也失去了最本質(zhì)的功能而已:復(fù)用臨時(shí)對(duì)象。調(diào)用 Pool.Put 調(diào)用的本質(zhì)目的就是為了對(duì)象復(fù)用


          3. Pool 本身允許復(fù)制之后使用嗎?

          不允許,但是你可以做的到。什么意思?

          如果你在代碼里 copy 了一個(gè) Pool 池,你的代碼 go build 是可以編譯通過的,但是可能會(huì)導(dǎo)致內(nèi)泄露的問題。在結(jié)構(gòu)體 struct Pool 的實(shí)現(xiàn)中中已經(jīng)明確說了,不允許 copy 。以下為官方原話:

          // A Pool must not be copied after first use.

          struct Pool 有一個(gè)字段 Pool.noCopy 明確限制你不要 copy,但是這個(gè)只有運(yùn)行 go vet 才能檢查出來(所以大家的代碼編譯之前一定要 go vet 做一次靜態(tài)檢查,可以避免非常多的問題)。

          $:~/pool$ go vet test_pool.go 
          # command-line-arguments
          ./test_pool.go:26:20: assignment copies lock value to bufferPool2: sync.Pool contains sync.noCopy

          思考下,為什么要 Pool 禁止 copy ?

          因?yàn)?Copy 之后,對(duì)于同一個(gè) Pool 里面 cache 的對(duì)象,我們有了兩個(gè)指向來源,原 Pool 清空之后,copy 的 Pool 沒有清理掉,那么里面的對(duì)象就全都泄露了。并且 Pool 里面的無鎖設(shè)計(jì)的基礎(chǔ)是多個(gè) Goroutine 不會(huì)操作到同一個(gè)數(shù)據(jù)結(jié)構(gòu),Pool 拷貝之后則不能保證這點(diǎn)。類似 sync.WaitGroup, sync.Cond 首字段都用了 noCopy  結(jié)構(gòu),所以這兩個(gè)結(jié)構(gòu)體也是不能 copy 使用的。

          所以,Pool 千萬不要 copy 使用,編譯之前一定要 go vet 檢查代碼。



          總結(jié)


          以上知識(shí)點(diǎn)做個(gè)總結(jié):

          1. Pool 本質(zhì)是為了提高臨時(shí)對(duì)象的復(fù)用率;
          2. Pool 使用兩層回收策略(local + victim)避免性能波動(dòng);
          3. Pool 本質(zhì)是一個(gè)雜貨鋪屬性,啥都可以放。把什么東西放進(jìn)去,預(yù)期從里面拿出什么類型的東西都需要業(yè)務(wù)使用方把控,Pool 池本身不做限制;
          4. Pool 池里面 cache 對(duì)象也是分層的,一層層的 cache,取用方式從最熱的數(shù)據(jù)到最冷的數(shù)據(jù)遞進(jìn);
          5. Pool 是并發(fā)安全的,但是內(nèi)部是無鎖結(jié)構(gòu),原理是對(duì)每個(gè) P 都分配 cache 數(shù)組( poolLocalInternal  數(shù)組),這樣 cache 結(jié)構(gòu)就不會(huì)導(dǎo)致并發(fā);
          6. 永遠(yuǎn)不要 copy 一個(gè) Pool,明確禁止,不然會(huì)導(dǎo)致內(nèi)存泄露和程序并發(fā)邏輯錯(cuò)誤;
          7. 代碼編譯之前用 go vet  做靜態(tài)檢查,能減少非常多的問題;
          8. 每輪 GC 開始都會(huì)清理一把 Pool 里面 cache 的對(duì)象,注意流程是分兩步,當(dāng)前 Pool 池 local 數(shù)組里的元素交給 victim 數(shù)組句柄,victim 里面 cache 的元素全部清理。換句話說,引入 victim 機(jī)制之后,對(duì)象的緩存時(shí)間變成兩個(gè) GC 周期;
          9. 不要對(duì) Pool 里面的對(duì)象做任何假定,有兩種方案:要么就歸還的時(shí)候 memset 對(duì)象之后,再調(diào)用 Pool.Put ,要么就 Pool.Get 取出來的時(shí)候 memset 之后再使用;
          10. 本篇文章配合動(dòng)畫演示一起學(xué)習(xí)效果更佳哦;




          推薦閱讀


          福利

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

          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  99re伊人 | 蜜臀久久精品久久久久宅男 | 操逼非常好非常棒的视频 | 波多野结衣在线精品 | 丁香五月婷婷啪啪 |