這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go tip 是 Go 語言的實驗分支,包含了很多尚在討論,但很有可能會加入 stable 分支的特性。“Go tip 在做什麼”(原文地址:What's happening in Go tip)分析總結了 Go 語言尚在開發中的一些重要特性。
本文譯自:What's happening in Go tip (2014-01-10)
現在是 2014 年了,剛剛經曆了聖誕和新年前夜,Go 團隊就已經開始為下一個發布版本而工作了。也因此,“Go tip 在做什麼”系列也重開了。
作為這個系列的最新一篇,這篇文章將會有些小調整。最重要的調整是,不會再遵循每周一篇文章的發布周期。一周裡可能有幾篇文章,也可能一篇都沒有。這個調整,一部分由於個人原因,一部分也因為這樣可以更靈活的追蹤 Go 的改變。這樣做的結果是,每篇文章可能會比以前更短,以便能緊跟最新的開發變化。
另一個調整是,將會覆蓋一些關於沒有變化的代碼的形成原因和討論。這是因為 Go 1.3 將會有重大改變(主要是計劃用 Go 重寫整個編譯器),有些代碼需要及早被大家瞭解。
這篇文章我們將會關注類型sync.Pool
。這個類型是 Go 1.3 標準庫新添加的第一個重要功能。
做了什麼
添加了sync.Pool
類型
相關 CL:CL 41860043, CL 43990043, CL 37720047, CL 44080043, CL 44150043, CL 44060044, CL 44050044,CL 44680043, CL 46010043
像 JVM 這種項目,花了很多的精力來改進垃圾收集系統,來保證其所要處理回收的眾多垃圾。另一方面 Go,大致上採用了在第一時間避免垃圾的設計方法,需要一個不那麼時髦的垃圾收集系統,來保證將記憶體的控制權交還給程式員。
由於這點,標準庫裡一些包分別實現了重用對象的池,來避免產生過多的垃圾。regexp
包為了保證並發時使用同一個正則,而維護了一組狀態機器,fmt
包有眾多的列印執行個體,其他包也有各自的池,或者可以採用這種技術。
不過,這種方法有兩個問題。最明顯的問題是代碼重複:即便重要的代碼大都相同,所有的包也需要實現一份自己的池。比較細微的問題是,沒有辦法回收池持有的空間。這種簡單的實現從來不會釋放記憶體,違反了使用記憶體回收的語言的原則,導致過高但不必要的記憶體使用量。
因為這些問題,Brad Fizpatrick曾建議在sync
包裡加入一個公開的Cache
類型。這個建議引發了一長串的討論。Go 語言應該在標準庫裡提供一個這個樣子的類型,還是應當將這個類型作為私下的實現?這個實現應該真的釋放記憶體嗎?如果釋放,什麼時候釋放?這個類型應當叫做Cache
,或者更應該叫做Pool
?
我先解釋一下緩衝(cache)和池(pool)的區別,以及為什麼這個區別對討論很重要。Brad Fizpatrick 建議的類型實際上是一種池:一組可以互換的值,取出時並不關心具體的值是什麼,因為每個值都是剛被初始化的狀態,值是相同的。你甚至分不出來剛剛拿到的值是從池裡取出來的,還是新建立的。另一方面,緩衝是一些相呼映射的鍵和值。一個明顯的例子是磁碟緩衝。磁碟緩衝將慢速儲存中的檔案快取在系統主記憶體裡,以便提高訪問速度。如果緩衝裡有對應鍵 A 和 B 的值(磁碟緩衝的例子裡,就是檔案名稱),而你請求了與 A 對應的值,你顯然不想得到 B 所對應的值。實際上,緩衝裡的值是互不相同的,增加了緩衝清除機制的複雜性,就是說到底哪個值應該被清除出緩衝。維基百科上關於緩衝演算法的頁面,列舉了 13 種不同的清除緩衝的演算法,從著名的 LRU 緩衝到更複雜的比如LIRS 緩衝演算法。
按照這種方式,我們的池真正要關心的問題,只是什麼時候回收池佔有的空間。而且大家提到了幾乎各種可能性:一些在 GC 前回收,一些在 GC 後,基於時鐘或者採用弱引用指標。所有的建議都有其弊病。
在經曆了漫長的討論後,Russ Cox 最終提議的 API 和回收策略非常簡單:在垃圾收集時回收池空間。這個建議提醒我們,類型Pool
的目的是在垃圾收集之間重用記憶體。它不應該避免記憶體回收,而是讓記憶體回收變得更有效。
實現了這個提議,並在幾次討論後,提交到 Go 的程式碼程式庫。當然,這個 CL 不是最終結果。首先,所有的池都要改寫為sync.Pool
。這些改寫由CL 43990043,CL 37720047,CL 44080043,CL 44150043,CL 44060044追蹤,但不包括CL 44050044。CL 44050044關注在嘗試將encoding/gob
包裡使用的本地釋放鏈表替換為sync.Pool
。本地是個關鍵詞。一個釋放鏈表會和一個解碼器(decoder)的生存時期一樣長,直到這個解碼器被銷毀,才會釋放這個鏈表。Russ Cox回複了這個 CL,明確了sync.Pool
的目的,以及它不能用來做什麼。直到這時,Rob Pike 提交並回複了CL 44680043,擴充了sync.Pool
類型的文檔,將其目的描述得更清楚。
Pool
設計用意是在全域變數裡維護的釋放鏈表,尤其是被多個 goroutine 同時訪問的全域變數。使用Pool
代替自己寫的釋放鏈表,可以讓程式啟動並執行時候,在恰當的情境下從池裡重用某項值。sync.Pool
一種合適的方法是,為臨時緩衝區建立一個池,多個用戶端使用這個緩衝區來共用全域資源。另一方面,如果釋放鏈表是某個對象的一部分,並由這個對象維護,而這個對象只由一個用戶端使用,在這個用戶端工作完成後釋放鏈表,那麼用Pool
實現這個釋放鏈表是不合適的。
從回複(和更早的討論)來看,加入sync.Pool
還是一種實驗,如果Pool
沒有實現它的功能,有可能發布 Go 1.3 之前將其完全移除。這件事情由Issue 6984跟蹤。
雖然本文對sync.Pool
的探索結束了,但是關於池的討論還沒有結束。還有CL 46010043,為了更適合并發時使用,改進了非常簡單的初始化實現。但這個 CL 在目前還沒有通過審核。
開發流程的小改變
從 Go 1.3 的周期開始,開發的流程有一些小的變化。這些變化只會影響到直接參与開發流程的人,以及像我一樣,緊跟最新變動的人。
啟用了一個新的郵件清單,golang-coderreviews,並作為新 CL 的預設抄送對象,替代了原有的golang-dev。這個想法是為了降低 golang-dev 裡的噪音,以便讓其關注真正的討論。
同時也啟用了一個新的資訊板,允許提交者更容易的跟蹤還在開放的 Issue 和 CL。任何對 Go 團隊的工作方式感興趣的人,都可以在這個新的資訊板上找到有用的說明。