sync.pool 原始碼閱讀
閱讀專案程式碼的時候發現很多地方用到了 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
- 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相等(實際上就是佇列已有的空間都有值了,滿了)
- 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
- poolLocal 是每個排程器 (P) 存 Object 的結構體
- private 是每個排程器私有的,shared 是所有排程器公有的,每個排程器 pop 時的邏輯是: 先看 private,沒有在看自己的 shared,再沒有就去其他排程器的 shared 偷,再沒有才是空
-
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 中
- 開啟搶佔並且 pool 加鎖然後關閉搶佔,這裡如果不先開啟搶佔的話,其他 goroutine 如果之前獲得鎖了,但不能執行,當前 goroutine 在獲取鎖,就會死鎖
- 如果判斷 pid 和 len([] poolLocal) 的關係,小於就返回 [PID] poolLocal
- 如果此 Pool 的 [] poolLocal 是空的,就把 Pool 加到 allPools 中
- 獲得當前 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 得到的物件有什麼關係
- 關掉競爭檢測
- 將 goroutine 固定到一個排程器 (P), 並獲取他的 poolLocal 和 PID
- 判斷該排程器 (P) 的 poolLocal 的私有空間是不是空的,如果是空的,就從該排程器 (P) 的 poolLocal shared 空間頭 pop 一下看有沒有
- 如果沒有,就說明該排程器 (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
}
懶獲取函式
- 取到 Pool 的 localSize 和 local
- 然後遍歷其他排程器 (P) 對應的 poolLocal,看看能不能從對應 poolLocal 中的 shared tail 中取出物件, 如果能取到,直接返回
- 如果取不到就到 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
}
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- ReactorKit原始碼閱讀React原始碼
- Vollery原始碼閱讀(—)原始碼
- NGINX原始碼閱讀Nginx原始碼
- ThreadLocal原始碼閱讀thread原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- Runtime 原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- AmplifyImpostors原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- fuzz原始碼閱讀原始碼
- HashMap 原始碼閱讀HashMap原始碼
- delta原始碼閱讀原始碼
- AQS原始碼閱讀AQS原始碼
- Mux 原始碼閱讀UX原始碼
- ConcurrentHashMap原始碼閱讀HashMap原始碼
- HashMap原始碼閱讀HashMap原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 使用OpenGrok閱讀原始碼原始碼
- express 原始碼閱讀(全)Express原始碼
- Kingfisher原始碼閱讀(一)原始碼
- 如何閱讀框架原始碼框架原始碼
- 如何閱讀jdk原始碼?JDK原始碼
- ArrayList原始碼閱讀(增)原始碼
- snabbdom 原始碼閱讀分析原始碼
- Appdash原始碼閱讀——reflectAPP原始碼
- React原始碼閱讀:setStateReact原始碼
- 如何快速閱讀原始碼原始碼
- 原始碼閱讀工具-understand原始碼
- koa原始碼閱讀[0]原始碼