Go 類型安全的 Pool
池(sync.Pool
)是一組可單獨保存 (Set
) 和檢索 (Get
) 的臨時對象集合。
存儲在池中的任何項都可能在任何時候自動移除而無需通知。如果池在移除項時持有該對象的唯一引用,那麼這個對象可能會被釋放掉。
池能夠確保在多個 goroutine 同時訪問時的安全性。
池的目的在於緩存已分配但未使用的對象以便後續複用,減輕垃圾收集器的壓力。
也就是說池的功能是爲了重用對象,目的是減輕 GC 的壓力。
類型不安全?
你看sync.Pool
提供的方法:
type Pool struct {
New func() any
}
func (p *Pool) Get() any
func (p *Pool) Put(x any)
它存儲的對象類型是any
,這樣的話,我們在使用的時候就需要進行類型轉換,這樣就會導致類型不安全,或者說使用起來很麻煩。
比就以官方的例子爲例:
package main
import (
"bytes"
"io"
"os"
"sync"
"time"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func timeNow() time.Time {
return time.Unix(1136214245, 0)
}
func Log(w io.Writer, key, val string) {
b := bufPool.Get().(*bytes.Buffer) // 類型轉換!!!!
b.Reset()
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
bufPool.Put(b)
}
func main() {
Log(os.Stdout, "path", "/search?q=flowers")
}
每次我們從sync.Pool
中獲取對象時,我們都需要進行類型轉換,有一點點麻煩,而且是非類型安全的,有潛在的風險,比如誤從另外一個包含其它類型的sync.Pool
中獲取對象。
其實我們可以使用泛型進行改造,但是爲啥官方實現沒有實現泛型呢?
那是因爲 Go 的泛型實現的比較晚,所以當時只能使用interface{}
(後來的any
類型)來實現泛型,這樣就會導致類型不安全。
類型安全的 Pool
我們可以通過泛型來解決這個問題,我們可以定義一個泛型的Pool
,這樣我們就可以直接使用泛型類型了。
事實上 mkmik/syncpool 就實現了一個泛型的Pool
,通過巧妙的包裝,簡單幾行代碼就實現了:
package syncpool
import (
"sync"
)
type Pool[T any] struct {
pool sync.Pool
}
func New[T any](fn func() T) Pool[T] {
return Pool[T]{
pool: sync.Pool{New: func() interface{} { return fn() }},
}
}
func (p *Pool[T]) Get() T {
return p.pool.Get().(T)
}
func (p *Pool[T]) Put(x T) {
p.pool.Put(x)
}
這裏你可能有個疑問,Get
方法在把接口類型轉換爲泛型類型時,爲什麼不需要進行錯誤檢查呢:
c, ok := p.pool.Get().(T)
嗯,其實是沒必要的,因爲我們的泛型 Pool 已經保證了保存的對象都是T
類型的。
我寫這篇文章主要源自 Phuong Le 最新的推文 "Golang Tip #71: sync.Pool, make it typed-safe with generics." 他的 Golang Tip 系列文章非常有價值,我已經獲得作者授權,後續會翻譯一些文章,希望對大家有所幫助。
還是有裝箱 / 拆箱操作
既然使用底層的snyc.Pool
, 那自然還有裝箱 / 拆箱操作,也就是說,當我們保存一個T
類型的對象,它會轉換成接口類型,當我們取出一個對象時,又會把接口類型轉換成T
類型。 從性能上講,這個操作是有開銷的,那麼sync.Pool
是否會修改成泛型呢,目前看是不會的,因爲 Go 要保持向下兼容,基於這個承諾,已經沒機會改了。
那麼我們能否基於sync.Pool
自己修改呢?難度很大,主要在於下面一點:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())
sync.Pool
在運行時中插入了一個樁子,運行時在垃圾回收的時候,會調用函數做對象的清理,而且這個函數是單例的,只處理sync.Pool
類型 (你新創建的 sync.Pool 都會放到一個全局列表中,被這個函數做對象回收)。
不是太容易hack
。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TW0IoOOwcSIeMK33L8Bx_w