Go 併發系列是根據我對晁嶽攀老師的《Go 併發編程實戰課》的吸取和理解整理而成,若有誤差,歡迎指正~
Go 是一個支持自動垃圾回收的語言,對程序員而言,咱們想建立對象就建立,不用關心資源的回收,大大提升了開發的效率。程序員
可是方便的背後,卻有也有不小的代價。Go 的垃圾回收機制仍是有一個 STW (stop-the-world,程序暫停)的時間,大量建立的對象,都會影響垃圾回收標記的時間。數據庫
除此以外,像數據庫的鏈接,tcp 鏈接,這些鏈接的建立自己就十分耗時,若是能將這些鏈接複用,也能減小業務耗時。編程
因此,高併發場景下,採用池化(Pool)手段,對某些對象集中管理,重複利用,減小建立和垃圾回收的成本,不只能夠大大提升業務的響應速度,也能提升程序的總體性能。緩存
池化就是對某些對象進行集中管理,重複利用,減小對象建立和垃圾回收的成本。安全
Go 標準庫 sync 提供了一個通用的 Pool,經過這個 Pool 能夠建立池化對象,實現通常對象的管理。數據結構
下面咱們主要看一下 sync.Pool 的實現。併發
sync.Pool 的使用很簡單,它有1個對外的成員變量 New 和2個對外的成員方法 Get 和 Put。tcp
下面是一個使用示例(見 fUsePool 方法):函數
type AFreeCoder struct { officialAccount string article string content \[\]string placeHolder string}// 爲了真實模擬,這裏禁止編譯器使用內聯優化//go:noinlinefunc NewAFreeCoder() \*AFreeCoder { return &AFreeCoder{ officialAccount: "碼農的自由之路", content: make(\[\]string, 10000, 10000), placeHolder: "若是以爲有用,歡迎關注哦~", }}func (a \*AFreeCoder) Write() { a.article = "Go 併發之性能提高殺器 Pool"}func f(concurrentNum int) { var w sync.WaitGroup w.Add(concurrentNum) for i := 0; i < concurrentNum; i++ { go func() { defer w.Done() a := NewAFreeCoder() a.Write() }() } w.Wait()}func fUsePool(concurrentNum int) { var w sync.WaitGroup p := sync.Pool{ New: func() interface{} { return NewAFreeCoder() }, } w.Add(concurrentNum) for i := 0; i < concurrentNum; i++ { go func() { defer w.Done() a := p.Get().(\*AFreeCoder) defer p.Put(a) a.Write() }() } w.Wait()}
AFreeCoder 是自定義的結構體,用來模擬初始化比較耗時類型。高併發
New 是函數類型變量,傳入的函數須要實現 AFreeCoder 的初始化。
Get 方法返回的是 interface{} 類型,須要斷言成 New 返回的類型。
Put 方法也比較好理解,變量用完了再放回去。
上面的示例中,f 和 fUsePool 分別實現了不使用 Pool 和使用 Pool 狀況下,併發執行 Write 函數的功能。
那麼這兩個函數的性能對好比何呢?咱們能夠用 go test 的 benchmark 測試一下(併發數 concurrentNum=100),測試結果以下:
goos: darwingoarch: amd64pkg: go\_practice/pool\_exampleBenchmark\_f-8 853 1355041 ns/op 16392237 B/op 203 allocs/opBenchmark\_fUsePool-8 12460 98046 ns/op 565066 B/op 9 allocs/opPASSok go\_practice/pool\_example 4.663s
測試結果顯示,使用了 Pool 以後,內存分配的次數相比不使用 Pool 的方式少不少,總體的耗時也會小不少。
若是把上面示例中初始化函數 NewAFreeCoder 中 content 的初始化操做去掉,再測試一次呢?測試結果以下:
goos: darwingoarch: amd64pkg: go\_practice/pool\_exampleBenchmark\_f-8 853 1355041 ns/op 16392237 B/op 203 allocs/opBenchmark\_fUsePool-8 12460 98046 ns/op 565066 B/op 9 allocs/opPASSok go\_practice/pool\_example 4.663s
上面數據粘錯了)使用了 Pool 以後,內存分配次數和每次操做消耗的內存仍然不多,可是總體的耗時相對不使用 Pool 的狀況並沒減小。
這是由於此時建立 AFreeCoder 對象的成本較低,而 Pool 相關操做也會有性能的消耗,因此才致使二者總體耗時差很少。
sync.Pool 自己是線程安全的,能夠多個 goroutine 併發調用,使用起來很方便,可是有兩個注意點:
第1點,禁止拷貝很好理解,畢竟 New 很容易修改。
第2點,不能存放須要保持長鏈接的對象。這是由於 sync.Pool 註冊了本身的 Pool 清理函數,Pool 中的變量可能會被垃圾回收掉。若是需求保存長鏈接,有不少其它的 Pool 實現了這種功能。
看一下 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{} }
noCopy 不用多解釋,用於靜態檢查,防拷貝的。New 前面也說過,用來存對象初始化函數的。
重點是 local 和 victim 這兩個字段。解釋這兩個字段前,先上一張《Go 併發實戰課》原文的 sync.Pool 數據結構的示意圖:
sync.Pool 中,緩存的對象並非存儲在一個隊列中,而是根據處理器 P 的核數 n 存了 n 份,這樣能最大程度的保證併發的時候 n 個 goroutine 能夠同時獲取對象。
local 和 victim 結構都同樣,都是 poolLocal 類型,有 private 和 shared 成員。private 存儲單個對象,shared 是 poolChain 類型,相似隊列,存了一堆對象。由於 local 和 victim 都是和處理器綁定的,當某個 goroutine 獨佔一個處理器時,直接經過 private 取值不須要加鎖,速度就會很快。
爲何有了 local,還須要 victim 呢?這是爲了下降池子中對象被回收的可能性。
因爲 sync.Pool 中存儲對象的個數不定,大小不定,因此它須要在系統閒暇的時候將變量回收掉。其實現方式以下:
func poolCleanup() { for \_, p := range oldPools { p.victim = nil p.victimSize = 0 } for \_, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } oldPools, allPools = allPools, nil }
poolClean() 函數被註冊到了 runtime 中,會在每一次 GC 調用以前被調用。這樣 GC 第一次調用的時候,local 雖然被清空,可是還能經過 victim 拿到池子中的對象。
Put 方法實現的功能是將用完的對象從新放回池子裏。由於 Put 比較簡單,因此先介紹 Put 方法。
Put 方法實現以下:
func (p \*Pool) Put(x interface{}) { if x == nil { return } // 把當前goroutine固定在當前的P上 // l 就是 local l, \_ := p.pin() if l.private == nil { l.private = x x = nil } if x != nil { l.shared.pushHead(x) } runtime\_procUnpin()}
先說一下 p.pin() 和 runtime\_procUnpin(), 這兩個函數分別實現了某 goroutine 搶佔當前 P(處理器)和解除搶佔的功能。因此這裏 private 的複製和以後 Get 方法中的讀取都不須要加鎖。
整個邏輯比較簡單,優先存到本地 private,若是 private 已經有值了,就放到本地隊列中。
Get 方法實現以下:
func (p \*Pool) Get() interface{} { // 把當前goroutine固定在當前的P上 l, pid := p.pin() x := l.private // 優先從local的private字段取,快速 l.private = nil if x == nil { // 從當前的local.shared彈出一個,注意是從head讀取並移除 x, \_ = l.shared.popHead() if x == nil { // 若是沒有,則去偷一個 x = p.getSlow(pid) } } runtime\_procUnpin() // 若是沒有獲取到,嘗試使用New函數生成一個新的 if x == nil && p.New != nil { x = p.New() } return x}
Get 方法總體歸納就是從池子中取出一個對象,若是沒有對象了,就 New 一個,再返回。
細節上,先從當前 P 對應的 local 的 private 獲取,獲取不到,就從當前 P 的 local 的隊列 shared 中獲取,還獲取不到就從其它 P 的 shared 中獲取 (getSlow 方法)。
若是最終仍然獲取不到,才 New 一個對象。
雖然 sync.Pool 也作了不少優化,性能有了很大的提高,可是使用的時候仍是有兩個坑:
內存泄露
若是池子中對象的類型是 slice,它的 cap 可能不斷的變大,而 sync.Pool 的回收機制(第二次回收)可能致使這些過大的對象愈來愈多,且一直沒法回收,最終形成內存泄露。
因此有一些特定 Pool 的使用中,會對池子中的變量的大小作一個限制,超過一個閾值直接丟棄。
內存浪費
除了內存泄露外,還有一種浪費的狀況,就是池子中的變量變得很大,可是不少時候只須要一個很小的變量,就會形成內存浪費的狀況。
由於 sync.Pool 保存的對象可能會被無通知的釋放掉,並不適合用來保存鏈接對象。鏈接對象的保存通常都經過其它方法完成。
好比 Go 中 http 鏈接的鏈接池的實如今 Transport 中,它用一個 idleConn 對象(map)來保存鏈接,因爲沒有相似 sync.Pool 的垃圾回收方法 PoolClean(),因此能保持長鏈接。Transport 對鏈接數量的控制經過 LRU 實現。
像第三方包 faith/pool,它是經過 channel + Mutex 的方式實現的 Pool,空閒的鏈接放到 channel 中。這也是 channel 的一個應用場景。
Pool 是一個通用的概念,也是解決對象重用和預先分配的一個經常使用的優化手段。
可是項目一開始其實不必考慮考慮這種優化,只有到了中後期階段,出現性能瓶頸,須要優化的時候,能夠考慮經過 Pool 的方式來優化。
碼農的自由之路
996的碼農,也能自由~
47篇原創內容
公衆號
都看到這裏了,不如點個 贊/在看,加個關注唄~~