Go Sync.Pool 背後的想法

我最近在一個項目中遇到了垃圾回收問題。大量對象被重複分配,並致使 GC 的巨大工做量。使用 sync.Pool,我可以減小分配和 GC 工做負載。緩存

什麼是 sync.Pool?

Go 1.3 版本的亮點之一是同步池。它是 sync 包下的一個組件,用於建立自我管理的臨時檢索對象池。安全

爲何要使用 sync.Pool?

咱們但願儘量減小 GC 開銷。頻繁的內存分配和回收會給 GC 帶來沉重的負擔。sync.Poll 能夠緩存暫時不使用的對象,並在下次須要時直接使用它們(無需從新分配)。這可能會減小 GC 工做負載並提升性能。ide

怎麼使用 sync.Pool?

首先,您須要設置新函數。當池中沒有緩存對象時將使用此函數。以後,您只須要使用 GetPut 方法來檢索和返回對象。另外,池在第一次使用後絕對不能複製。函數

因爲 New 函數類型是 func() interface{}Get 方法返回一個 interface{}。爲了獲得具體對象,你須要作一個類型斷言。性能

`// A dummy struct`
`type Person struct {`
 `Name string`
`}`
`// Initializing pool`
`var personPool = sync.Pool{`
 `// New optionally specifies a function to generate`
 `// a value when Get would otherwise return nil.`
 `New: func() interface{} { return new(Person) },`
`}`
`// Main function`
`func main() {`
 `// Get hold of an instance`
 `newPerson := personPool.Get().(*Person)`
 `// Defer release function`
 `// After that the same instance is`
 `// reusable by another routine`
 `defer personPool.Put(newPerson)`
 `// Using the instance`
 `newPerson.Name = "Jack"`
`}`

基準測試

`type Person struct {`
 `Age int`
`}`
`var personPool = sync.Pool{`
 `New: func() interface{} { return new(Person) },`
`}`
`func BenchmarkWithoutPool(b *testing.B) {`
 `var p *Person`
 `b.ReportAllocs()`
 `b.ResetTimer()`
 `for i := 0; i < b.N; i++ {`
 `for j := 0; j < 10000; j++ {`
 `p = new(Person)`
 `p.Age = 23`
 `}`
 `}`
`}`
`func BenchmarkWithPool(b *testing.B) {`
 `var p *Person`
 `b.ReportAllocs()`
 `b.ResetTimer()`
 `for i := 0; i < b.N; i++ {`
 `for j := 0; j < 10000; j++ {`
 `p = personPool.Get().(*Person)`
 `p.Age = 23`
 `personPool.Put(p)`
 `}`
 `}`
`}`

測試結果:測試

`BenchmarkWithoutPool`
`BenchmarkWithoutPool-8   160698 ns/op   80001 B/op   10000 allocs/op`
`BenchmarkWithPool`
`BenchmarkWithPool-8      191163 ns/op       0 B/op       0 allocs/op`

權衡

生活中的一切都是一種權衡。池也有它的性能成本。使用 sync.Pool 比簡單的初始化要慢得多。idea

`func BenchmarkPool(b *testing.B) {`
 `var p sync.Pool`
 `b.RunParallel(func(pb *testing.PB) {`
 `for pb.Next() {`
 `p.Put(1)`
 `p.Get()`
 `}`
 `})`
`}`
`func BenchmarkAllocation(b *testing.B) {`
 `b.RunParallel(func(pb *testing.PB) {`
 `for pb.Next() {`
 `i := 0`
 `i = i`
 `}`
 `})`
`}`

壓測結果:spa

`BenchmarkPool`
`BenchmarkPool-8           283395016          4.40 ns/op`
`BenchmarkAllocation`
`BenchmarkAllocation-8    1000000000         0.344 ns/op`

sync.Pool 是如何工做的?

sync.Pool 有兩個對象容器: 本地池 (活動) 和受害者緩存 (存檔)。線程

根據 sync/pool.go ,包 init 函數做爲清理池的方法註冊到運行時。此方法將由 GC 觸發。code

`func init() {`
 `runtime_registerPoolCleanup(poolCleanup)`
`}`

當 GC 被觸發時,受害者緩存中的對象將被收集,而後本地池中的對象將被移動到受害者緩存中。

`func poolCleanup() {`
 `// 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`
 `}`
 `oldPools, allPools = allPools, nil`
`}`

新對象被放入本地池中。調用 Put 方法也會將對象放入本地池中。調用 Get 方法將首先從受害者緩存中獲取對象,若是受害者緩存爲空,則對象將從本地池中獲取。

圖片

供你參考,Go 1.12 sync.pool 實現使用基於 mutex 的鎖,用於來自多個 Goroutines 的線程安全操做。Go 1.13 引入了一個雙鏈表做爲共享池,它刪除了 mutex 並改善了共享訪問。

結論

當有一個昂貴的對象須要頻繁建立時,使用 sync.Pool 是很是有益的。

譯自:https://medium.com/swlh/go-th...

圖片

來都來了,點個「再看」再走叭~~             

圖片

相關文章
相關標籤/搜索