1. 程式人生 > >[Go] sync.Pool 的實現原理 和 適用場景

[Go] sync.Pool 的實現原理 和 適用場景

臨時 digg 簡單的 設置 com 運行 之前 結果 官方文檔

摘錄一:

Go 1.3 的 sync 包中加入一個新特性:Pool。

官方文檔可以看這裏 http://golang.org/pkg/sync/#Pool

這個類設計的目的是用來保存和復用臨時對象,以減少內存分配,降低CG壓力。

type Pool  
    func (p *Pool) Get() interface{}  
    func (p *Pool) Put(x interface{})  
    New func() interface{}  

Get 返回 Pool 中的任意一個對象。

如果 Pool 為空,則調用 New 返回一個新創建的對象。

如果沒有設置 New,則返回 nil。

還有一個重要的特性是,放進 Pool 中的對象,會在說不準什麽時候被回收掉。

所以如果事先 Put 進去 100 個對象,下次 Get 的時候發現 Pool 是空也是有可能的。

不過這個特性的一個好處就在於不用擔心 Pool 會一直增長,因為 Go 已經幫你在 Pool 中做了回收機制。

這個清理過程是在每次垃圾回收之前做的。垃圾回收是固定兩分鐘觸發一次。

而且每次清理會將 Pool 中的所有對象都清理掉!

package main

import(
    "sync"
    "log"
)

func main(){
    // 建立對象
    var pipe = &sync.Pool{New:func()interface{}{return "Hello, BeiJing"}}
    
    // 準備放入的字符串
    val := "Hello,World!"
    
    // 放入
    pipe.Put(val)
    
    // 取出
    log.Println(pipe.Get())
    
    // 再取就沒有了,會自動調用NEW
    log.Println(pipe.Get())
}

// 輸出
2014/09/30 15:43:30 Hello, World!
2014/09/30 15:43:30 Hello, BeiJing

摘自:http://www.nljb.net/default/sync.Pool/

摘錄二:

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

先來看看如何使用一個 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)  
}  

上面創建了一個緩存 int 對象的一個 pool,先從池獲取一個對象然後放進去一個對象再取出一個對象,程序的輸出是 0 1。創建的時候可以指定一個 New 函數,獲取對象的時候如何在池裏面找不到緩存的對象將會使用指定的 new 函數創建一個返回,如果沒有 new 函數則返回 nil。用法是不是很簡單,我們這裏就不多說,下面來說說我們關心的問題:

1、緩存對象的數量和期限

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

func init() {  
    runtime_registerPoolCleanup(poolCleanup)  
}  

可以看到 pool 包在 init 的時候註冊了一個 poolCleanup 函數,它會清除所有的 pool 裏面的所有緩存的對象,該函數註冊進去之後會在每次 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)  

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

2、緩存對象的開銷

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

技術分享

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

獲取對象過程是:

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

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

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

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

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

歸還對象的過程:

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

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

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

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

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

摘自:http://blog.csdn.net/yongjian_lian/article/details/42058893

[Go] sync.Pool 的實現原理 和 適用場景