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