池(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
,这样我们就可以直接使用泛型类型了。
事实上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
。