參考
go語言的官方包sync.Pool的實(shí)現(xiàn)原理和適用場(chǎng)景
深入Golang之sync.Pool詳解
偽共享(false sharing)袜茧,并發(fā)編程無聲的性能殺手
一涤垫、簡(jiǎn)述
眾所周知惠啄,go是自動(dòng)垃圾回收的(garbage collector)场仲,這大大減少了程序編程負(fù)擔(dān)橄唬。但gc是一把雙刃劍法竞,帶來了編程的方便但同時(shí)也增加了運(yùn)行時(shí)開銷耙厚,使用不當(dāng)甚至?xí)?yán)重影響程序的性能。因此性能要求高的場(chǎng)景不能任意產(chǎn)生太多的垃圾(有g(shù)c但又不能完全依賴它挺惡心的)岔霸,如何解決呢薛躬?那就是要重用對(duì)象了,我們可以簡(jiǎn)單的使用一個(gè)chan把這些可重用的對(duì)象緩存起來呆细,但如果很多goroutine競(jìng)爭(zhēng)一個(gè)chan性能肯定是問題.....由于golang團(tuán)隊(duì)認(rèn)識(shí)到這個(gè)問題普遍存在型宝,為了避免大家重造車輪,因此官方統(tǒng)一出了一個(gè)包Pool絮爷。但為什么放到sync包里面也是有的迷惑的趴酣,先不討論這個(gè)問題。
先來看看如何使用一個(gè)pool:
package main
import(
"fmt"
"sync"
)
func main() {
p := &sync.Pool{
New: func() interface{} {
return 0
},
}
a := p.Get().(int)
p.Put(1)
b := p.Get().(int)
fmt.Println(a, b)
}
上面創(chuàng)建了一個(gè)緩存int對(duì)象的一個(gè)pool坑夯,先從池獲取一個(gè)對(duì)象然后放進(jìn)去一個(gè)對(duì)象再取出一個(gè)對(duì)象价卤,程序的輸出是0 1。創(chuàng)建的時(shí)候可以指定一個(gè)New函數(shù)渊涝,獲取對(duì)象的時(shí)候如何在池里面找不到緩存的對(duì)象將會(huì)使用指定的new函數(shù)創(chuàng)建一個(gè)返回慎璧,如果沒有new函數(shù)則返回nil。用法是不是很簡(jiǎn)單跨释,我們這里就不多說胸私,下面來說說我們關(guān)心的問題:
1、緩存對(duì)象的數(shù)量和期限
上面我們可以看到pool創(chuàng)建的時(shí)候是不能指定大小的鳖谈,所有sync.Pool的緩存對(duì)象數(shù)量是沒有限制的(只受限于內(nèi)存)岁疼,因此使用sync.pool是沒辦法做到控制緩存對(duì)象數(shù)量的個(gè)數(shù)的。另外sync.pool緩存對(duì)象的期限是很詭異的缆娃,先看一下src/pkg/sync/pool.go里面的一段實(shí)現(xiàn)代碼:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
可以看到pool包在init的時(shí)候注冊(cè)了一個(gè)poolCleanup函數(shù)捷绒,它會(huì)清除所有的pool里面的所有緩存的對(duì)象,該函數(shù)注冊(cè)進(jìn)去之后會(huì)在每次gc之前都會(huì)調(diào)用贯要,因此sync.Pool緩存的期限只是兩次gc之間這段時(shí)間暖侨。例如我們把上面的例子改成下面這樣之后,輸出的結(jié)果將是0 0崇渗。正因gc的時(shí)候會(huì)清掉緩存對(duì)象字逗,也不用擔(dān)心pool會(huì)無限增大的問題京郑。
a := p.Get().(int)
p.Put(1)
runtime.GC()
b := p.Get().(int)
fmt.Println(a, b)
這是很多人錯(cuò)誤理解的地方,正因?yàn)檫@樣葫掉,我們是不可以使用sync.Pool去實(shí)現(xiàn)一個(gè)socket連接池的些举。
2、緩存對(duì)象的開銷
如何在多個(gè)goroutine之間使用同一個(gè)pool做到高效呢俭厚?官方的做法就是盡量減少競(jìng)爭(zhēng)户魏,因?yàn)閟ync.pool為每個(gè)P(對(duì)應(yīng)cpu,不了解的童鞋可以去看看golang的調(diào)度模型介紹)都分配了一個(gè)子池挪挤,如下圖:
當(dāng)執(zhí)行一個(gè)pool的get或者put操作的時(shí)候都會(huì)先把當(dāng)前的goroutine固定到某個(gè)P的子池上面绪抛,然后再對(duì)該子池進(jìn)行操作。每個(gè)子池里面有一個(gè)私有對(duì)象和共享列表對(duì)象电禀,私有對(duì)象是只有對(duì)應(yīng)的P能夠訪問,因?yàn)橐粋€(gè)P同一時(shí)間只能執(zhí)行一個(gè)goroutine笤休,因此對(duì)私有對(duì)象存取操作是不需要加鎖的尖飞。共享列表是和其他P分享的,因此操作共享列表是需要加鎖的店雅。
獲取對(duì)象過程是:
1)固定到某個(gè)P政基,嘗試從私有對(duì)象獲取,如果私有對(duì)象非空則返回該對(duì)象闹啦,并把私有對(duì)象置空沮明;
2)如果私有對(duì)象是空的時(shí)候,就去當(dāng)前子池的共享列表獲惹戏堋(需要加鎖)荐健;
3)如果當(dāng)前子池的共享列表也是空的,那么就嘗試去其他P的子池的共享列表偷取一個(gè)(需要加鎖)琳袄;
4)如果其他子池都是空的江场,最后就用用戶指定的New函數(shù)產(chǎn)生一個(gè)新的對(duì)象返回。
可以看到一次get操作最少0次加鎖窖逗,最大N(N等于MAXPROCS)次加鎖址否。
歸還對(duì)象的過程:
1)固定到某個(gè)P,如果私有對(duì)象為空則放到私有對(duì)象碎紊;
2)否則加入到該P(yáng)子池的共享列表中(需要加鎖)佑附。
可以看到一次put操作最少0次加鎖,最多1次加鎖仗考。
由于goroutine具體會(huì)分配到那個(gè)P執(zhí)行是golang的協(xié)程調(diào)度系統(tǒng)決定的音同,因此在MAXPROCS>1的情況下,多goroutine用同一個(gè)sync.Pool的話秃嗜,各個(gè)P的子池之間緩存的對(duì)象是否平衡以及開銷如何是沒辦法準(zhǔn)確衡量的瘟斜。但如果goroutine數(shù)目和緩存的對(duì)象數(shù)目遠(yuǎn)遠(yuǎn)大于MAXPROCS的話缸夹,概率上說應(yīng)該是相對(duì)平衡的。
總的來說螺句,sync.Pool的定位不是做類似連接池的東西虽惭,它的用途僅僅是增加對(duì)象重用的幾率,減少gc的負(fù)擔(dān)蛇尚,而開銷方面也不是很便宜的芽唇。
二、源碼
sync.Pool首先聲明了兩個(gè)結(jié)構(gòu)體
// Local per-P Pool appendix.
type poolLocalInternal struct {
private interface{} // Can be used only by the respective P.
shared []interface{} // Can be used by any P.
Mutex // Protects shared.
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
為了使得在多個(gè)goroutine中高效的使用goroutine取劫,sync.Pool為每個(gè)P(對(duì)應(yīng)CPU)都分配一個(gè)本地池匆笤,當(dāng)執(zhí)行Get或者Put操作的時(shí)候,會(huì)先將goroutine和某個(gè)P的子池關(guān)聯(lián)谱邪,再對(duì)該子池進(jìn)行操作炮捧。 每個(gè)P的子池分為私有對(duì)象和共享列表對(duì)象,私有對(duì)象只能被特定的P訪問惦银,共享列表對(duì)象可以被任何P訪問咆课。因?yàn)橥粫r(shí)刻一個(gè)P只能執(zhí)行一個(gè)goroutine,所以無需加鎖书蚪,但是對(duì)共享列表對(duì)象進(jìn)行操作時(shí),因?yàn)榭赡苡卸鄠€(gè)goroutine同時(shí)操作迅栅,所以需要加鎖殊校。
值得注意的是poolLocal結(jié)構(gòu)體中有個(gè)pad成員,目的是為了防止false sharing读存。cache使用中常見的一個(gè)問題是false sharing为流。當(dāng)不同的線程同時(shí)讀寫同一cache line上不同數(shù)據(jù)時(shí)就可能發(fā)生false sharing。false sharing會(huì)導(dǎo)致多核處理器上嚴(yán)重的系統(tǒng)性能下降让簿。具體的可以參考偽共享(false sharing)艺谆,并發(fā)編程無聲的性能殺手
類型sync.Pool有兩個(gè)公開的方法,一個(gè)是Get拜英,一個(gè)是Put, 我們先來看一下Put的源碼静汤。
// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
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
}
runtime_procUnpin()
if x != nil {
l.Lock()
l.shared = append(l.shared, x)
l.Unlock()
}
if race.Enabled {
race.Enable()
}
}
1.如果放入的值為空,直接return.
2.檢查當(dāng)前goroutine的是否設(shè)置對(duì)象池私有值居凶,如果沒有則將x賦值給其私有成員虫给,并將x設(shè)置為nil。
3.如果當(dāng)前goroutine私有值已經(jīng)被設(shè)置侠碧,那么將該值追加到共享列表抹估。
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
l := p.pin()
x := l.private
l.private = nil
runtime_procUnpin()
if x == nil {
l.Lock()
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
}
l.Unlock()
if x == nil {
x = p.getSlow()
}
}
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
if x == nil && p.New != nil {
x = p.New()
}
return x
}
1.嘗試從本地P對(duì)應(yīng)的那個(gè)本地池中獲取一個(gè)對(duì)象值, 并從本地池沖刪除該值。
2.如果獲取失敗弄兜,那么從共享池中獲取, 并從共享隊(duì)列中刪除該值药蜻。
3.如果獲取失敗瓷式,那么從其他P的共享池中偷一個(gè)過來,并刪除共享池中的該值(p.getSlow())语泽。
4.如果仍然失敗贸典,那么直接通過New()分配一個(gè)返回值,注意這個(gè)分配的值不會(huì)被放入池中踱卵。New()返回用戶注冊(cè)的New函數(shù)的值廊驼,如果用戶未注冊(cè)New,那么返回nil惋砂。
三妒挎、總結(jié)
通過以上的解讀,我們可以看到西饵,Get方法并不會(huì)對(duì)獲取到的對(duì)象值做任何的保證酝掩,因?yàn)榉湃氡镜爻刂械闹涤锌赡軙?huì)在任何時(shí)候被刪除,但是不通知調(diào)用者眷柔。放入共享池中的值有可能被其他的goroutine偷走期虾。 所以對(duì)象池比較適合用來存儲(chǔ)一些臨時(shí)切狀態(tài)無關(guān)的數(shù)據(jù),但是不適合用來存儲(chǔ)數(shù)據(jù)庫(kù)連接的實(shí)例闯割,因?yàn)榇嫒雽?duì)象池重的值有可能會(huì)在垃圾回收時(shí)被刪除掉,這違反了數(shù)據(jù)庫(kù)連接池建立的初衷竿拆。
根據(jù)上面的說法宙拉,Golang的對(duì)象池嚴(yán)格意義上來說是一個(gè)臨時(shí)的對(duì)象池,適用于儲(chǔ)存一些會(huì)在goroutine間分享的臨時(shí)對(duì)象丙笋。主要作用是減少GC谢澈,提高性能。在Golang中最常見的使用場(chǎng)景是fmt包中的輸出緩沖區(qū)御板。
在Golang中如果要實(shí)現(xiàn)連接池的效果锥忿,可以用container/list來實(shí)現(xiàn),開源界也有一些現(xiàn)成的實(shí)現(xiàn)怠肋,比如go-commons-pool敬鬓,具體的讀者可以去自行了解。