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

你們好,我是 frank。
歡迎你們點擊上方藍色文字「Golang 語言開發棧」關注公衆號。

01golang

介紹編程

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併發

使用方式app

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

  • func (p *Pool) Put(x interface{})
  • func (p *Pool) Get() interface{}

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 運行時每次在執行垃圾回收以前,先執行該清理函數。

示例代碼:

`func main () {`
 `pool := &sync.Pool{`
 `New: func() interface{} {`
 `fmt.Println("New 一個新對象")`
 `return 0`
 `},`
 `}`
 `// 取,臨時對象池中沒有數據,會調用 New,New 建立一個新對象直接返回,不會存儲在臨時對象池中`
 `val := pool.Get().(int)`
 `fmt.Println(val)`
 `// 存`
 `pool.Put(10)`
 `// 手動調用 GC(),用於驗證 GC 以後,臨時對象池中的對象會被清空。`
 `runtime.GC()`
 `// 取`
 `val2 := pool.Get().(int)`
 `fmt.Println(val2)`
`}`

03

實現原理

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

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

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

`type Pool struct {`
 `noCopy noCopy`
 `local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal`
 `localSize uintptr        // size of the local array`
 `victim     unsafe.Pointer // local from previous cycle`
 `victimSize uintptr        // size of victims array`
 `// New optionally specifies a function to generate`
 `// a value when Get would otherwise return nil.`
 `// It may not be changed concurrently with calls to Get.`
 `New func() interface{}`
`}`

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

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

`func poolCleanup() {`
 `// This function is called with the world stopped, at the beginning of a garbage collection.`
 `// It must not allocate and probably should not call any runtime functions.`
 `// Because the world is stopped, no pool user can be in a`
 `// pinned section (in effect, this has all Ps pinned).`
 `// Drop victim caches from all pools.`
 `for _, p := range oldPools {`
 `p.victim = nil`
 `p.victimSize = 0`
 `}`
 `// Move primary cache to victim cache.`
 `for _, p := range allPools {`
 `p.victim = p.local`
 `p.victimSize = p.localSize`
 `p.local = nil`
 `p.localSize = 0`
 `}`
 `// The pools with non-empty primary caches now have non-empty`
 `// victim caches and no pools have primary caches.`
 `oldPools, allPools = allPools, nil`
`}`

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

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

閱讀上面這段代碼,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() 方法的處理邏輯:

`func (p *Pool) getSlow(pid int) interface{} {`
 `// See the comment in pin regarding ordering of the loads.`
 `size := runtime_LoadAcquintptr(&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`
`}`

04

總結

本文咱們主要介紹了 sync.Pool 數據類型,包括它的使用方式和實現原理,它的優點就是能夠複用對象,下降對象的新建和 GC 的開銷。咱們須要再次強調的是,sync.Pool 的生命週期受 GC 的影響,不適合用來作須要本身管理生命週期的池化,好比鏈接池。

推薦閱讀:

Go 語言使用標準庫 sync 包的 mutex 互斥鎖解決數據競態

Golang 語言標準庫 sync 包的 RWMutex 讀寫互斥鎖怎麼使用?

Golang語言標準庫 sync 包的 WaitGroup 怎麼使用?

Golang語言標準庫 sync 包的 Cond 怎麼使用?

Golang語言標準庫 sync 包的 Once 怎麼使用?

Golang 語言標準庫 sync/atomic 包原子操做

Golang 語言標準庫 context 包控制 goroutine

Golang 語言使用 channel 併發編程

Golang 語言中 map 的鍵值類型選擇,它是併發安全的嗎?

參考資料:
https://golang.org/pkg/sync/#...
https://golang.org/src/sync/p...

圖片

掃描二維碼,加入微信羣

圖片

點「贊」和「在看」是最大的支持👇

圖片

👇更多精彩內容,請點擊閱讀原文」

相關文章
相關標籤/搜索