737
阿裏雲
技術社區[雲棲]
《GO並發編程實戰》—— 臨時對象池
本章要講解的是sync.Pool類型。我們可以把sync.Pool類型值看作是存放可被重複使用的值的容器。此類容器是自動伸縮的、高效的,同時也是並發安全的。為了描述方便,我們也會把sync.Pool類型的值稱為臨時對象池,而把存於其中的值稱為對象值。至於為什麼要加“臨時“這兩個字,我們稍後再解釋。
我們在用複合字麵量初始化一個臨時對象池的時候可以為它唯一的公開字段New賦值。該字段的類型是func() interface{},即一個函數類型。可以猜到,被賦給字段New的函數會被臨時對象池用來創建對象值。不過,實際上,該函數幾乎僅在池中無可用對象值的時候才會被調用。
類型sync.Pool有兩個公開的方法。一個是Get,另一個是Put。前者的功能是從池中獲取一個interface{}類型的值,而後者的作用則是把一個interface{}類型的值放置於池中。
通過Get方法獲取到的值是任意的。如果一個臨時對象池的Put方法未被調用過,且它的New字段也未曾被賦予一個非nil的函數值,那麼它的Get方法返回的結果值就一定會是nil。我們稍後會講到,Get方法返回的不一定就是存在於池中的值。不過,如果這個結果值是池中的,那麼在該方法返回它之前就一定會把它從池中刪除掉。
這樣一個臨時對象池在功能上看似與一個通用的緩存池相差無幾。但是實際上,臨時對象池本身的特性決定了它是一個“個性”非常鮮明的同步工具。我們在這裏說明它的兩個非常突出的特性。
第一個特性是,臨時對象池可以把由其中的對象值產生的存儲壓力進行分攤。更進一步說,它會專門為每一個與操作它的Goroutine相關聯的P都生成一個本地池。在臨時對象池的Get方法被調用的時候,它一般會先嚐試從與本地P對應的那個本地池中獲取一個對象值。如果獲取失敗,它就會試圖從其他P的本地池中偷一個對象值並直接返回給調用方。如果依然未果,那它隻能把希望寄托於當前的臨時對象池的New字段代表的那個對象值生成函數了。注意,這個對象值生成函數產生的對象值永遠不會被放置到池中。它會被直接返回給調用方。另一方麵,臨時對象池的Put方法會把它的參數值存放到與當前P對應的那個本地池中。每個P的本地池中的絕大多數對象值都是被同一個臨時對象池中的所有本地池所共享的。也就是說,它們隨時可能會被偷走。
臨時對象池的第二個突出特性是對垃圾回收友好。垃圾回收的執行一般會使臨時對象池中的對象值被全部移除。也就是說,即使我們永遠不會顯式的從臨時對象池取走某一個對象值,該對象值也不會永遠待在臨時對象池中。它的生命周期取決於垃圾回收任務下一次的執行時間。
請讀者閱讀一下這段代碼:
12 |
// 禁用GC,並保證在main函數執行結束前恢複GC
|
13 |
defer debug.SetGCPercent(debug.SetGCPercent(- 1 ))
|
15 |
newFunc := func() interface {} {
|
16 |
return atomic.AddInt32(&count, 1 )
|
18 |
pool := sync.Pool{New: newFunc}
|
22 |
fmt.Printf( "v1: %v\n" , v1)
|
29 |
fmt.Printf( "v2: %v\n" , v2)
|
32 |
debug.SetGCPercent( 100 )
|
35 |
fmt.Printf( "v3: %v\n" , v3)
|
38 |
fmt.Printf( "v4: %v\n" , v4)
|
在這裏,我們使用runtime/debug代碼包的SetGCPercent函數來禁用、恢複GC以及指定垃圾收集比率(詳見第7章的第1節中的相關說明),以保證我們的演示能夠如願進行。
我們把這段代碼存放在gocp項目的sync1/pool代碼包的文件pool_demo.go中,並使用go run命令運行它。就像下麵這樣:
hc@ubt:~/golang/goc2p/src/sync1/pool$ go run pool_demo.go
而後,我們會在標準輸出上看到如下內容:
請讀者注意第3行和第4行的內容,也就是我們在手動的進行垃圾回收之後的輸出內容。在把nil賦給pool的New字段之前,即使手動的執行了垃圾回收,我們也是可以從臨時對象池獲取到一個對象值的。而在這之後,我們卻隻能取出nil。讀者應該可以依據我們剛剛描述的那兩個特性想明白如此輸出的原因。
看到這裏,讀者可能會隱約的感覺到,我們在使用臨時對象池的時候應該依照一些方式方法,否則就會很容易邁入陷坑。實際情況確實如此。
首先,我們不能對通過Get方法獲取到的對象值有任何假設。到底哪一個值會被取出是完全不確定的。這是因為我們總是不能得知操作臨時對象池的Goroutine在哪一時刻會與哪一個P相關聯,尤其是在比上述示例更加複雜的程序的運行過程中。在這種情況下,我們也就無從知曉我們放入的對象值會被存放到哪一個本地池中,以及哪一個Goroutine執行的Get方法會返回該對象值。所以,我們給予臨時對象池的對象值生成函數所產生的值以及通過調用它的Put方法放入到池中的值都應該是無狀態的或者狀態一致的。從另一方麵說,我們在取出並使用這些值的時候也不應該以其中的任何狀態作為先決條件。這一點非常的重要。
第二個需要注意的地方實際上與我們前麵講到的第二個特性緊密相關。臨時對象池中的任何對象值都有可能在任何時候被移除掉,並且根本不會通知該池的使用方。這種情況常常會發生在垃圾回收器即將開始回收內存垃圾的時候。如果這時臨時對象池中的某個對象值僅被該池引用,那麼它還可能會在垃圾回收的時候被回收掉。因此,我們也就不能假設之前放入到臨時對象池的某個對象值會一直待在池中,即使我們沒有顯式的把它從池中取出。甚至一個對象值可以在臨時對象池中待多久,我們也無法假設。除非我們像前麵的示例那樣手動的控製GC的啟停。不過,我們並不推薦這種方式。這會帶來一些其他問題。
依據我們剛剛講述的臨時對象池特性和使用注意事項,讀者應該可以想象得出臨時對象池的一些適用場景(比如作為臨時且狀態無關的數據的暫存處),以及一些不適用的場景(比如用來存放數據庫連接的實例)。如果我們在做實現技術的選型的時候把臨時對象池作為了候選之一,那麼就應該好好想想它的“個性”是不是符合你的需要。如果真的適合,那麼它的特性一定會為你的程序增光添彩,無論在功能上還是在性能上。而如果它被用在了不恰當的地方,那麼就隻能適得其反了。
最後更新:2017-05-23 12:02:37