已經(jīng)使用golang有一段時(shí)間偷俭,go的協(xié)程和gc垃圾回收特性的確會(huì)提高程序的開(kāi)發(fā)效率淌哟。但是畢竟是一門(mén)新語(yǔ)言贞绵,如果對(duì)于它的機(jī)制不了解慎冤,用起來(lái)可能會(huì)蹦出各種潘多拉盒子严望。今天就講講我在項(xiàng)目中用到的sync包的Pool類(lèi)的使用村象,以免大家混淆使用。
眾所周知鹅很,go是自動(dòng)垃圾回收的(garbage collector)嘶居,這大大減少了程序編程負(fù)擔(dān)。但gc是一把雙刃劍促煮,帶來(lái)了編程的方便但同時(shí)也增加了運(yùn)行時(shí)開(kāi)銷(xiāo)邮屁,使用不當(dāng)甚至?xí)?yán)重影響程序的性能。因此性能要求高的場(chǎng)景不能任意產(chǎn)生太多的垃圾(有g(shù)c但又不能完全依賴(lài)它挺惡心的)菠齿,如何解決呢佑吝?那就是要重用對(duì)象了,我們可以簡(jiǎn)單的使用一個(gè)chan把這些可重用的對(duì)象緩存起來(lái)绳匀,但如果很多goroutine競(jìng)爭(zhēng)一個(gè)chan性能肯定是問(wèn)題…由于golang團(tuán)隊(duì)認(rèn)識(shí)到這個(gè)問(wèn)題普遍存在芋忿,為了避免大家重造車(chē)輪,因此官方統(tǒng)一出了一個(gè)包Pool襟士。但為什么放到sync包里面也是有的迷惑的盗飒,先不討論這個(gè)問(wèn)題嚷量。
先來(lái)看看如何使用一個(gè)pool:
packagemainimport("fmt""sync")funcmain(){p:=&sync.Pool{New:func()interface{}{return0},}a:=p.Get().(int)p.Put(1)b:=p.Get().(int)fmt.Println(a,b)}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面創(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è)返回,如果沒(méi)有new函數(shù)則返回nil抖所。用法是不是很簡(jiǎn)單梨州,我們這里就不多說(shuō),下面來(lái)說(shuō)說(shuō)我們關(guān)心的問(wèn)題:
1田轧、緩存對(duì)象的數(shù)量和期限
上面我們可以看到pool創(chuàng)建的時(shí)候是不能指定大小的暴匠,所有sync.Pool的緩存對(duì)象數(shù)量是沒(méi)有限制的(只受限于內(nèi)存),因此使用sync.pool是沒(méi)辦法做到控制緩存對(duì)象數(shù)量的個(gè)數(shù)的傻粘。另外sync.pool緩存對(duì)象的期限是很詭異的每窖,先看一下src/pkg/sync/pool.go里面的一段實(shí)現(xiàn)代碼:
funcinit(){runtime_registerPoolCleanup(poolCleanup)}
1
2
3
可以看到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ì)無(wú)限增大的問(wèn)題。
a:=p.Get().(int)p.Put(1)runtime.GC()b:=p.Get().(int)fmt.Println(a,b)
1
2
3
4
5
這是很多人錯(cuò)誤理解的地方劈猪,正因?yàn)檫@樣昧甘,我們是不可以使用sync.Pool去實(shí)現(xiàn)一個(gè)socket連接池的。
2战得、緩存對(duì)象的開(kāi)銷(xiāo)
如何在多個(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能夠訪(fǎng)問(wèn),因?yàn)橐粋€(gè)P同一時(shí)間只能執(zhí)行一個(gè)goroutine杀捻,因此對(duì)私有對(duì)象存取操作是不需要加鎖的井厌。共享列表是和其他P分享的,因此操作共享列表是需要加鎖的致讥。
獲取對(duì)象過(guò)程是:
1)固定到某個(gè)P仅仆,嘗試從私有對(duì)象獲取,如果私有對(duì)象非空則返回該對(duì)象垢袱,并把私有對(duì)象置空墓拜;
2)如果私有對(duì)象是空的時(shí)候,就去當(dāng)前子池的共享列表獲惹肫酢(需要加鎖)咳榜;
3)如果當(dāng)前子池的共享列表也是空的,那么就嘗試去其他P的子池的共享列表偷取一個(gè)(需要加鎖)爽锥;
4)如果其他子池都是空的涌韩,最后就用用戶(hù)指定的New函數(shù)產(chǎn)生一個(gè)新的對(duì)象返回。
可以看到一次get操作最少0次加鎖氯夷,最大N(N等于MAXPROCS)次加鎖臣樱。
歸還對(duì)象的過(guò)程:
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的話(huà),各個(gè)P的子池之間緩存的對(duì)象是否平衡以及開(kāi)銷(xiāo)如何是沒(méi)辦法準(zhǔn)確衡量的席吴。但如果goroutine數(shù)目和緩存的對(duì)象數(shù)目遠(yuǎn)遠(yuǎn)大于MAXPROCS的話(huà)赌结,概率上說(shuō)應(yīng)該是相對(duì)平衡的。
總的來(lái)說(shuō)孝冒,sync.Pool的定位不是做類(lèi)似連接池的東西柬姚,它的用途僅僅是增加對(duì)象重用的幾率,減少gc的負(fù)擔(dān)庄涡,而開(kāi)銷(xiāo)方面也不是很便宜的量承。