go語言的官方包sync.Pool的實現(xiàn)原理和適用場景

已經(jīng)使用golang有一段時間,go的協(xié)程和gc垃圾回收特性的確會提高程序的開發(fā)效率。但是畢竟是一門新語言,如果對于它的機制不了解,用起來可能會蹦出各種潘多拉盒子。今天就講講我在項目中用到的sync包的Pool類的使用,以免大家混淆使用。

眾所周知,go是自動垃圾回收的(garbage collector),這大大減少了程序編程負擔。但gc是一把雙刃劍,帶來了編程的方便但同時也增加了運行時開銷,使用不當甚至會嚴重影響程序的性能。因此性能要求高的場景不能任意產生太多的垃圾(有gc但又不能完全依賴它挺惡心的),如何解決呢?那就是要重用對象了,我們可以簡單的使用一個chan把這些可重用的對象緩存起來,但如果很多goroutine競爭一個chan性能肯定是問題…由于golang團隊認識到這個問題普遍存在,為了避免大家重造車輪,因此官方統(tǒng)一出了一個包Pool。但為什么放到sync包里面也是有的迷惑的,先不討論這個問題。

先來看看如何使用一個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)建了一個緩存int對象的一個pool,先從池獲取一個對象然后放進去一個對象再取出一個對象,程序的輸出是0 1。創(chuàng)建的時候可以指定一個New函數(shù),獲取對象的時候如何在池里面找不到緩存的對象將會使用指定的new函數(shù)創(chuàng)建一個返回,如果沒有new函數(shù)則返回nil。用法是不是很簡單,我們這里就不多說,下面來說說我們關心的問題:

1、緩存對象的數(shù)量和期限

上面我們可以看到pool創(chuàng)建的時候是不能指定大小的,所有sync.Pool的緩存對象數(shù)量是沒有限制的(只受限于內存),因此使用sync.pool是沒辦法做到控制緩存對象數(shù)量的個數(shù)的。另外sync.pool緩存對象的期限是很詭異的,先看一下src/pkg/sync/pool.go里面的一段實現(xiàn)代碼:

funcinit(){runtime_registerPoolCleanup(poolCleanup)}

1

2

3

可以看到pool包在init的時候注冊了一個poolCleanup函數(shù),它會清除所有的pool里面的所有緩存的對象,該函數(shù)注冊進去之后會在每次gc之前都會調用,因此sync.Pool緩存的期限只是兩次gc之間這段時間。例如我們把上面的例子改成下面這樣之后,輸出的結果將是0 0。正因gc的時候會清掉緩存對象,也不用擔心pool會無限增大的問題。

a:=p.Get().(int)p.Put(1)runtime.GC()b:=p.Get().(int)fmt.Println(a,b)

1

2

3

4

5

這是很多人錯誤理解的地方,正因為這樣,我們是不可以使用sync.Pool去實現(xiàn)一個socket連接池的。

2、緩存對象的開銷

如何在多個goroutine之間使用同一個pool做到高效呢?官方的做法就是盡量減少競爭,因為sync.pool為每個P(對應cpu,不了解的童鞋可以去看看golang的調度模型介紹)都分配了一個子池,如下圖:

當執(zhí)行一個pool的get或者put操作的時候都會先把當前的goroutine固定到某個P的子池上面,然后再對該子池進行操作。每個子池里面有一個私有對象和共享列表對象,私有對象是只有對應的P能夠訪問,因為一個P同一時間只能執(zhí)行一個goroutine,因此對私有對象存取操作是不需要加鎖的。共享列表是和其他P分享的,因此操作共享列表是需要加鎖的。

獲取對象過程是:

1)固定到某個P,嘗試從私有對象獲取,如果私有對象非空則返回該對象,并把私有對象置空;

2)如果私有對象是空的時候,就去當前子池的共享列表獲取(需要加鎖);

3)如果當前子池的共享列表也是空的,那么就嘗試去其他P的子池的共享列表偷取一個(需要加鎖);

4)如果其他子池都是空的,最后就用用戶指定的New函數(shù)產生一個新的對象返回。

可以看到一次get操作最少0次加鎖,最大N(N等于MAXPROCS)次加鎖。

歸還對象的過程:

1)固定到某個P,如果私有對象為空則放到私有對象;

2)否則加入到該P子池的共享列表中(需要加鎖)。

可以看到一次put操作最少0次加鎖,最多1次加鎖。

由于goroutine具體會分配到那個P執(zhí)行是golang的協(xié)程調度系統(tǒng)決定的,因此在MAXPROCS>1的情況下,多goroutine用同一個sync.Pool的話,各個P的子池之間緩存的對象是否平衡以及開銷如何是沒辦法準確衡量的。但如果goroutine數(shù)目和緩存的對象數(shù)目遠遠大于MAXPROCS的話,概率上說應該是相對平衡的。

總的來說,sync.Pool的定位不是做類似連接池的東西,它的用途僅僅是增加對象重用的幾率,減少gc的負擔,而開銷方面也不是很便宜的。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容