sync.pool 原始碼閱讀

journey-c發表於2020-05-15

閱讀專案程式碼的時候發現很多地方用到了 golang 的 sync.pool,所以好奇 golang 的 sync.pool 底層實現是什麼樣的,有哪些優化。 本文是基於 go1.13.8,做講解。


介紹

Pool 翻譯過來就是池子,主要功能就是: 需要使用某個 Object 的時候可以從 Pool 獲取,使用完畢再歸還,從而減少建立和銷燬 Object 的開銷。而本文講的就是 golang 中的 Pool 原始碼實現。

用法

千萬不要想當然的認為 put 進去的 Object 和 get 出來的 Object 有什麼關係,Pool 存的 Object 在 GC 時會都清理掉

package main

import (
    "fmt"
    "sync"
)

type Book struct {
    Name string
    Info map[string]string
}

func NewBook() interface{} {
    return &Book{
        Name: "",
        Info: make(map[string]string),
    }
}

func main() {
    // 建立pool並定義建立object的函式
    bookPool := sync.Pool{New:NewBook}

    // pool獲取object
    a := bookPool.Get().(*Book)
    a.Name = "go"
    a.Info["a"] = "b"

    fmt.Println(a)

    // 放回pool
    bookPool.Put(a)
}

結構圖

實現細節

  • Pool 實現原始碼是這兩個檔案 go/src/sync/pool.go, go/src/sync/poolqueue.go

資料結構——從下往上講一下 Pool 底層儲存是如何實現

eface

// 儲存元素的結構體,型別指標和值指標
type eface struct {
        typ, val unsafe.Pointer
}

Pool 底層用 eface 來儲存單個 Object, 包括 typ 指標: Object 的型別,val 指標: Object 的值

poolDequeue

poolDequeue 是一個無鎖、固定大小的單生產端多消費端的環形佇列,單一 producer 可以在頭部 push 和 pop(可能和傳統佇列頭部只能 push 的定義不同),多 consumer 可以在尾部 pop

  1. headTail:
[hhhhhhhh hhhhhhhh hhhhhhhh hhhhhhhh tttttttt tttttttt tttttttt tttttttt] 
1. headTail表示下標,高32位表示頭下標,低32位表示尾下標,poolDequeue定義了,head tail的pack和unpack函式方便轉化,
    實際用的時候都會mod ( len(vals) - 1 ) 來防止溢位
2. head和tail永遠只用32位表示,溢位後會從0開始,這也滿足迴圈佇列的設計
3. 佇列為空的條件  tail == head
4. 佇列滿的條件    (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head tail加上佇列長度和head相等(實際上就是佇列已有的空間都有值了,滿了)
  1. vals:

1) poolDequeue 是被 poolChain 使用,poolChain 使用 poolDequeue 時 a) 初始化 vals 長度為 8,vals 長度必須是 2 的冪 b) 當佇列滿時,vals 長度*2,最大擴充套件到 dequeueLimit = (1 << 32) / 4 = (1 << 30),之後就不會擴充套件了

2) 為什麼 vals 長度必須是 2 的冪 這是因為 go 的記憶體管理策略是將記憶體分為 2 的冪大小的連結串列,申請 2 的冪大小的記憶體可以有效減小分配記憶體的開銷

3) 為什麼 dequeueLimit 是 (1 << 32) / 4 = 1 << 30 a) dequeueLimit 必須是 2 的冪 (上邊解釋過) b) head 和 tail 都是 32 位,最大是 1 << 31,如果都用的話,head 和 tail 就是無符號整型,無符號整型使用的時候會有很多上溢的錯誤,這類錯誤是不容易檢測的,所以相比之下還不如用 31 位有符號整型,有錯就報出來,結論參考https://stackoverrun.com/cn/q/10770747

type poolDequeue struct {
    headTail uint64

    vals []eface
}

// poolDequeue成員函式
// 這裡的刪除操作,是將指標置空,然後讓GC來回收記憶體空間
unpack     將headTail分解為head和tail
pack       將head和tail組合成headTail
pushHead   新增元素到隊首
popHead    獲取並刪除隊首元素
popTail    獲取並刪除隊尾元素
PushHead   新增元素到隊首
PopHead    獲取並刪除隊首元素
PopTail    獲取並刪除隊尾元素

poolChainElt

連結串列的一個節點 Node

type poolChainElt struct {
    poolDequeue

    // next and prev link to the adjacent poolChainElts in this
    // poolChain.
    //
    // next is written atomically by the producer and read
    // atomically by the consumer. It only transitions from nil to
    // non-nil.
    //
    // prev is written atomically by the consumer and read
    // atomically by the producer. It only transitions from
    // non-nil to nil.
    next, prev *poolChainElt
}

poolChain

poolChain 是動態版的 poolDequeue head(poolDequeue)[prev] --> <--- next[prev] ---> <---[next] tail(poolDequeue) 動態的佇列,佇列每個節點又是一個環形佇列 (poolDequeue)

type poolChain struct {
    // 頭指標,只能單一producer操作(push, pop)
    head *poolChainElt

    // 尾指標,可以被多個consumer pop,必須是原子操作
    tail *poolChainElt
}

// poolChain成員函式
func (c *poolChain) pushHead(val interface{})
    1. 如果head為nil,說明佇列現在是空的,那麼新建一個節點,將head和tail都指向這個節點
    2. 將val push到head的環形佇列中,如果push成功了,可以返回了
    3. 如果沒push成功,則說明head的環形佇列滿了,就再建立一個兩倍head大小的節點[最大(1 << 32) / 4],
        將新節點作為head,並且處理好新head和舊head的next,prev關係
    4. 將val push到head的環形佇列中

func (c *poolChain) popHead()
    1. 先在head環形佇列中popHead試試,如果空了,當前節點就沒用了,就刪掉當前節點,去prev節點並且把prev節點作為新head再取一值遞迴下去,
        能取到就返回,取不到說明佇列空了
func (c *poolChain) popTail()
    1. 如果tail為nil,說明佇列是空的,直接返回
    2. 如果tail非nil,就取取試試,有東西就返回
    3. 如果沒取出來東西,那麼說明tail節點沒存東西了,遞迴去prev節點環形佇列中popTail,並且把prev節點作為tail,能取到就返回,取不到就是空了

poolLocal

  1. poolLocal 是每個排程器 (P) 存 Object 的結構體
  2. private 是每個排程器私有的,shared 是所有排程器公有的,每個排程器 pop 時的邏輯是: 先看 private,沒有在看自己的 shared,再沒有就去其他排程器的 shared 偷,再沒有才是空
  3. pad 是防止偽共享,參考https://www.cnblogs.com/cyfonly/p/5800758.html

    type poolLocal struct {
    poolLocalInternal
    
    // Prevents false sharing on widespread platforms with
    // 128 mod (cache line size) = 0 .
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
    }
    

// Local per-P Pool appendix. // 當前排程器的內部資源 type poolLocalInternal struct { // 當前排程器的私有資源 private interface{} // Can be used only by the respective P. // 所有排程器的公有資源 shared poolChain // Local P can pushHead/popHead; any P can popTail. }


## 主要函式

### Put

Put adds x to the pool.
1. 首先關閉競爭檢測,然後會將當前goroutine固定到一個排程器(P)上,且不允許搶佔
2. 從Pool的local中取出來當前goroutine固定到那個排程器(P)對應的poolLocal, 沒有就新建
3. 先判斷這個當前排程器(P)專屬poolLocal,私有空間是不是空的,如果是把x放到私有空間,並把x置nil
4. 判斷x是否為nil,如果不為空說明私有空間滿了,就push到該排程器專屬poolLocal的shared head
5. 允許搶佔,開啟競爭檢測

```go
func (p *Pool) Put(x interface{}) {
    // 如果put進來的值為空直接返回
    if x == nil {
        return
    }
    // 關閉競爭檢測
    if race.Enabled {
        if fastrand()%4 == 0 {
            // Randomly drop x on floor.
            return
        }
        race.ReleaseMerge(poolRaceAddr(x))
        race.Disable()
    }
    // 
    l, _ := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    if x != nil {
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
    if race.Enabled {
        race.Enable()
    }
}

把當前的 goroutine 固定到排程器 (P),不允許搶佔, 返回該排程器 (P) 對應的 poolLocal 和排程器 (P) ID 執行時排程器的三個重要組成部分 — 執行緒 M、Goroutine G 和排程器 P(負責排程)

判斷 pid 是否小於 [] poolLocal 的長度,小於的話就在取出 poolLocal[P] 返回,否則就去執行 pinSlow 函式 Caller must call runtime_procUnpin() when done with the pool.

func (p *Pool) pin() (*poolLocal, int) {
    // 關閉搶佔,等這個goroutine工作完,其他goroutine才能獲得時間片工作
    pid := runtime_procPin()
    // In pinSlow we store to local and then to localSize, here we load in opposite order.
    // Since we've disabled preemption, GC cannot happen in between.
    // Thus here we must observe local at least as large localSize.
    // We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).

    s := atomic.LoadUintptr(&p.localSize) // load-acquire
    l := p.local                          // load-consume
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid
    }
    return p.pinSlow()
}

當 goroutine 固定到的排程器 (P) 沒有 poolLocal 時,pins() 函式就會呼叫 pinSlow() 來重新固定到其他排程器 (P), 如果新固定到的排程器 (P) 還是沒有 poolLocal,就給該排程器建立一個 poolLocal 放到 Pool 的 local 中

  1. 開啟搶佔並且 pool 加鎖然後關閉搶佔,這裡如果不先開啟搶佔的話,其他 goroutine 如果之前獲得鎖了,但不能執行,當前 goroutine 在獲取鎖,就會死鎖
  2. 如果判斷 pid 和 len([] poolLocal) 的關係,小於就返回 [PID] poolLocal
  3. 如果此 Pool 的 [] poolLocal 是空的,就把 Pool 加到 allPools 中
  4. 獲得當前 cpu 的數量,建立一個 cpu 數量大小的 [] poolLocal
func (p *Pool) pinSlow() (*poolLocal, int) {
    runtime_procUnpin()
    allPoolsMu.Lock()
    defer allPoolsMu.Unlock()
    pid := runtime_procPin()
    // poolCleanup won't be called while we are pinned.
    s := p.localSize
    l := p.local
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid
    }
    if p.local == nil {
        allPools = append(allPools, p)
    }
    // If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
    size := 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
}

Get

從 Pool 中獲取物件,然後返回,如果 Pool 為空的就用 New 來建立 不要假設 Put 進來的物件和 Get 得到的物件有什麼關係

  1. 關掉競爭檢測
  2. 將 goroutine 固定到一個排程器 (P), 並獲取他的 poolLocal 和 PID
  3. 判斷該排程器 (P) 的 poolLocal 的私有空間是不是空的,如果是空的,就從該排程器 (P) 的 poolLocal shared 空間頭 pop 一下看有沒有
  4. 如果沒有,就說明該排程器 (P) 自己的 poolLocal 沒有物件了,就呼叫 getSlow
func (p *Pool) Get() interface{} {
    if race.Enabled {
        race.Disable()
    }
    l, pid := p.pin()
    x := l.private
    l.private = nil
    if x == nil {
        // Try to pop the head of the local shard. We prefer
        // the head over the tail for temporal locality of
        // reuse.
        x, _ = l.shared.popHead()
        if x == nil {
            x = p.getSlow(pid)
        }
    }
    runtime_procUnpin()
    if race.Enabled {
        race.Enable()
        if x != nil {
            race.Acquire(poolRaceAddr(x))
        }
    }
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

懶獲取函式

  1. 取到 Pool 的 localSize 和 local
  2. 然後遍歷其他排程器 (P) 對應的 poolLocal,看看能不能從對應 poolLocal 中的 shared tail 中取出物件, 如果能取到,直接返回
  3. 如果取不到就到 victim 中查詢,有就返回,沒有呼叫 New 建立一個新的 Object 返回
func (p *Pool) getSlow(pid int) interface{} {
    // See the comment in pin regarding ordering of the loads.
    size := atomic.LoadUintptr(&p.localSize) // load-acquire
    locals := p.local                        // load-consume
    // Try to steal one element from other procs.
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i+1)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // Try the victim cache. We do this after attempting to steal
    // from all primary caches because we want objects in the
    // victim cache to age out if at all possible.
    size = atomic.LoadUintptr(&p.victimSize)
    if uintptr(pid) >= size {
        return nil
    }
    locals = p.victim
    l := indexLocal(locals, pid)
    if x := l.private; x != nil {
        l.private = nil
        return x
    }
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // Mark the victim cache as empty for future gets don't bother
    // with it.
    atomic.StoreUintptr(&p.victimSize, 0)

    return nil
}

附錄

pool.dot

digraph {
    bgcolor="#C6CFD532";

    node [shape=record, fontsize="8", margin="0.04", height=0.2, color=gray]
    edge [fontname="Inconsolata, Consolas", fontsize=10, arrowhead=normal]

    pool [shape=record,label="{noCopy|<local>local|localSize|<victim>victim|victimSize|New}",xlabel="Pool"]
    poolLocal[shape=record,label="{<poolLocalInternal>poolLocalInternal|pad}",xlabel="poolLocal"]
    poolLocalInternal[shape=record,label="{private|<shared>shared}",xlabel="poolLocalInternal"]
    poolChain[shape=record,label="{<head>head|<tail>tail}",xlabel="poolChain"]
    poolChainElt[shape=record,label="{<poolDequeue>poolDequeue|next|prev}",xlabel="poolChainElt"]
    poolDequeue[shape=record,label="{headTail|<vals>vals}",xlabel="poolDequeue"]
    eface[shape=record,label="{typ|val}",xlabel="eface"]
    victim[shape=record,label="GC的時候,首先把local中每個處理器(P)對應的poolLocal賦給victim,然後清空local,所以victim就是快取GC前的local",xlabel="victim"]

    pool:local -> poolLocal [label="local指標指向[]poolLocal首地址",rankdir=LR]
    poolLocal:poolLocalInternal -> poolLocalInternal
    poolLocalInternal:shared -> poolChain[label="shared是一個佇列"]
    poolChain:head -> poolChainElt[label="head和tail是佇列的收尾節點指標"]
    poolChain:tail -> poolChainElt
    poolChainElt:poolDequeue -> poolDequeue[label="poolDequeue是一個環形佇列"]
    poolDequeue:vals -> eface[label="eface儲存Object的結構體,typ和val是Object的型別和值指標"]
    pool:victim -> victim
}
更多原創文章乾貨分享,請關注公眾號
  • sync.pool 原始碼閱讀
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章