Golang 語言臨時對象池 - sync-Pool

01

介紹

sync.Pool 是 sync 包提供的一個數據類型,也稱爲臨時對象池,它的值是用來存儲一組可以獨立訪問的臨時對象,它通過池化減少申請新對象,提升程序的性能。sync.Pool 類型是 struct 類型,它的值在被首次使用之後,就不可以再被複制了。因爲 sync.Pool 中存儲的所有對象都可以隨時自動刪除,所以使用 sync.Pool 類型的值必須滿足兩個條件,一是該值存在與否,都不會影響程序的功能,二是該值之間可以互相替代。sync.Pool 是 goroutine 併發安全的,可以安全地同時被多個 goroutine 使用;sync.Pool 的目的是緩存已分配但未使用的對象以供以後重用,從而減輕了垃圾收集器的性能影響,因爲 Go 的自動垃圾回收機制,會有一個 STW 的時間消耗,並且大量在堆上創建對象,也會增加垃圾回收標記的時間。

sync.Pool 的適當用法是管理一組臨時對象,這些臨時對象在程序包的併發獨立客戶端之間靜默共享並有可能被重用。sync.Pool 提供了一種分攤許多客戶端上的分配開銷的方法。

但是,作爲短期(short-lived)對象的一部分維護的空閒列表不適用於 sync.Pool,因爲在這種情況下,開銷無法很好地分攤。

Golang 語言中的標準庫 fmt 包使用了 sync.Pool,它會使用一個動態大小的 buffer 池做輸出緩存,當大量的 goroutine 併發輸出的時候,就會創建比較多的 buffer,並且在不需要的時候回收掉。

02

使用方式

sync.Poll 類型包含兩個方法:

Put() 用於向臨時對象池中存放對象,它接收一個 interface{} 空接口類型的參數;Get()用於從臨時對象池中獲取對象,它返回一個 interface{} 空接口類型的返回值。

Get() 從臨時對象池中選擇一個任意對象,將其從臨時對象池中刪除,然後將其返回給調用方。 Get() 可以選擇忽略臨時對象池並將其視爲空。調用者不應假定傳遞給 Put() 的值和 Get() 返回的值之間有任何關係。

如果 Get() 返回 nil,而 p.New 不爲 nil,則 Get() 返回調用 p.New 的結果。 sync.Pool 類型的 New 字段,字段類型是函數類型 func() interface{},代表創建臨時對象的函數,該函數的結果值並不會存入到臨時對象池中,而是直接返回給 Get() 方法的調用方。

需要注意的是,sync.Pool 類型的 New 字段的值也需要我們初始化對象時給定,否則,在調用 Get() 方法時,有可能會得到 nil。

我們已經介紹了臨時對象什麼時候會被創建,現在我們介紹臨時對象什麼時候會被銷燬。我們已經知道 sync.Pool 使用之前需要先初始化,其實在初始化時,還會向 Golang 運行時中註冊一個清理函數,用於清理臨時對象池中的所有已創建的值,golang 運行時每次在執行垃圾回收之前,先執行該清理函數。

示例代碼:

 1func main () {
 2 pool := &sync.Pool{
 3  New: func() interface{} {
 4   fmt.Println("New 一個新對象")
 5   return 0
 6  },
 7 }
 8
 9 // 取,臨時對象池中沒有數據,會調用 New,New 創建一個新對象直接返回,不會存儲在臨時對象池中
10 val := pool.Get().(int)
11 fmt.Println(val)
12
13 // 存
14 pool.Put(10)
15 // 手動調用 GC(),用於驗證 GC 之後,臨時對象池中的對象會被清空。
16 runtime.GC()
17
18 // 取
19 val2 := pool.Get().(int)
20 fmt.Println(val2)
21}
22
23

03

實現原理

在 Go1.13 之前,臨時對象池的數據結構中有一個本地池列表,在每個本地池中包含三個字段,分別是存儲私有臨時對象的字段 private、共享臨時對象列表的字段 shared 和 sync.Mutex 類型的嵌入字段。

鎖競爭會降低程序的併發性能,想要優化程序的併發性能,就是減少或避免鎖的使用。在 Go1.13 中,sync.Pool 做了優化,就是避免使用鎖,將加鎖的隊列改成了無鎖的隊列,並給即將被移除的元素多一次 “復活” 的機會。

當前 sync.Pool 的數據結構如下:

 1type Pool struct {
 2 noCopy noCopy
 3
 4 local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
 5 localSize uintptr        // size of the local array
 6
 7 victim     unsafe.Pointer // local from previous cycle
 8 victimSize uintptr        // size of victims array
 9
10 // New optionally specifies a function to generate
11 // a value when Get would otherwise return nil.
12 // It may not be changed concurrently with calls to Get.
13 New func() interface{}
14}
15
16

其中 local 和 victim 主要用於存儲空閒元素,每次 GC 時,Pool 會先把 victim 字段的數據移除,然後把 local 字段的數據給 victim,這樣 local 等於被清空了,而 local 的數據在 victim 中就有機會再次被 Get() 取走,如果沒有 Get() 取走數據,victim 的數據就會被 GC 掉。

閱讀下面這段代碼,它是 GC 時 sync.Pool 的處理邏輯。

 1func poolCleanup() {
 2 // This function is called with the world stopped, at the beginning of a garbage collection.
 3 // It must not allocate and probably should not call any runtime functions.
 4
 5 // Because the world is stopped, no pool user can be in a
 6 // pinned section (in effect, this has all Ps pinned).
 7
 8 // Drop victim caches from all pools.
 9 for _, p := range oldPools {
10  p.victim = nil
11  p.victimSize = 0
12 }
13
14 // Move primary cache to victim cache.
15 for _, p := range allPools {
16  p.victim = p.local
17  p.victimSize = p.localSize
18  p.local = nil
19  p.localSize = 0
20 }
21
22 // The pools with non-empty primary caches now have non-empty
23 // victim caches and no pools have primary caches.
24 oldPools, allPools = allPools, nil
25}
26
27

本地池列表中本地池的數量和 golang 調度器中 processor 的數量相等,也就是說每個本地池對應一個 P,我們在介紹 GMP 的文章中講過,一個 goroutine 想要運行,必須先和某個 P 關聯。所以臨時對象池的 Put()和 Get() 方法被調用時,會去操作哪個本地池,就取決於調用代碼運行的 goroutine 對應的 P,這就是爲什麼每個本地池對應一個 P。

 1// Local per-P Pool appendix.
 2type poolLocalInternal struct {
 3 private interface{} // Can be used only by the respective P.
 4 shared  poolChain   // Local P can pushHead/popHead; any P can popTail.
 5}
 6
 7type poolLocal struct {
 8 poolLocalInternal
 9
10 // Prevents false sharing on widespread platforms with
11 // 128 mod (cache line size) = 0 .
12 pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
13}
14
15

閱讀上面這段代碼,poolLocalInternal 結構體中包含兩個字段 private 和 shared,private 代表一個緩存元素,只能被當前 P 的 goroutine 存取,因爲一個 P 同時只能執行一個 goroutine,所以不會有併發問題。shared 可以被任意 P 訪問,但是隻能本地 P 可以 pushHead/popHead,其他 P 只能 popTail,它是使用一個無鎖隊列實現的。

存取數據:

Put() 方法會優先將新創建的臨時對象存儲在本地的 private 字段,如果 private 字段已經存儲了某個值,它纔會去訪問 shared 字段,把新的臨時對象追加到共享臨時對象列表的末尾。

Get() 方法會優先訪問 private 字段獲取數據,因爲無鎖,獲取元素的速度快,如果 private 字段爲空時,就會嘗試訪問 local 的 shared 字段,如果 local 的 shared 字段也是空的,它會調用 getSlow() 方法,遍歷每一個 local 的 shared 字段,只要發現某個 local 的 shared 字段有值,就會獲取該 shared 共享臨時對象列表的最後一個值並返回。如果遍歷所有 local 都沒有找到值,就會嘗試訪問 victim,先從 victim 的 private 字段中查找,如果沒有找到,再從 victim 的 shared 字段查找,最後,如果都沒有獲取到,就會調用初始化時的 New 字段給定的創建臨時對象的函數創建一個新對象並返回,如果 New 字段的值爲 nil,Get() 方法就直接返回 nil。

getSlow() 方法的處理邏輯:

 1func (p *Pool) getSlow(pid int) interface{} {
 2 // See the comment in pin regarding ordering of the loads.
 3 size := runtime_LoadAcquintptr(&p.localSize) // load-acquire
 4 locals := p.local                            // load-consume
 5 // Try to steal one element from other procs.
 6 for i := 0; i < int(size); i++ {
 7  l := indexLocal(locals, (pid+i+1)%int(size))
 8  if x, _ := l.shared.popTail(); x != nil {
 9   return x
10  }
11 }
12
13 // Try the victim cache. We do this after attempting to steal
14 // from all primary caches because we want objects in the
15 // victim cache to age out if at all possible.
16 size = atomic.LoadUintptr(&p.victimSize)
17 if uintptr(pid) >= size {
18  return nil
19 }
20 locals = p.victim
21 l := indexLocal(locals, pid)
22 if x := l.private; x != nil {
23  l.private = nil
24  return x
25 }
26 for i := 0; i < int(size); i++ {
27  l := indexLocal(locals, (pid+i)%int(size))
28  if x, _ := l.shared.popTail(); x != nil {
29   return x
30  }
31 }
32
33 // Mark the victim cache as empty for future gets don't bother
34 // with it.
35 atomic.StoreUintptr(&p.victimSize, 0)
36
37 return nil
38}
39
40

04

總結

本文我們主要介紹了 sync.Pool 數據類型,包括它的使用方式和實現原理,它的優勢就是可以複用對象,降低對象的新建和 GC 的開銷。我們需要再次強調的是,sync.Pool 的生命週期受 GC 的影響,不適合用來做需要自己管理生命週期的池化,比如連接池。

參考資料:
https://golang.org/pkg/sync/#Pool
https://golang.org/src/sync/pool.go?s=1633:2101#L34

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QH_IW-hUfxs2MxkLEBiexA