池(sync.Pool
)是一组可单独保存(Set
)和检索(Get
)的临时对象集合。
存储在池中的任何项都可能在任何时候自动移除而无需通知。如果池在移除项时持有该对象的唯一引用,那么这个对象可能会被释放掉。
池能够确保在多个goroutine同时访问时的安全性。
池的目的在于缓存已分配但未使用的对象以便后续复用,减轻垃圾收集器的压力。
也就是说池的功能是为了重用对象,目的是减轻GC的压力。
你看sync.Pool
提供的方法:
|
|
它存储的对象类型是any
,这样的话,我们在使用的时候就需要进行类型转换,这样就会导致类型不安全,或者说使用起来很麻烦。
比就以官方的例子为例:
|
|
每次我们从sync.Pool
中获取对象时,我们都需要进行类型转换,有一点点麻烦,而且是非类型安全的,有潜在的风险,比如误从另外一个包含其它类型的sync.Pool
中获取对象。
其实我们可以使用泛型进行改造,但是为啥官方实现没有实现泛型呢?
那是因为Go的泛型实现的比较晚,所以当时只能使用interface{}
(后来的any
类型)来实现泛型,这样就会导致类型不安全。
我们可以通过泛型来解决这个问题,我们可以定义一个泛型的Pool
,这样我们就可以直接使用泛型类型了。
事实上mkmik/syncpool就实现了一个泛型的Pool
,通过巧妙的包装,简单几行代码就实现了:
|
|
这里你可能有个疑问,Get
方法在把接口类型转换为泛型类型时,为什么不需要进行错误检查呢:
|
|
嗯,其实是没必要的,因为我们的泛型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
自己修改呢?难度很大,主要在于下面一点:
|
|
sync.Pool
在运行时中插入了一个桩子,运行时在垃圾回收的时候,会调用函数做对象的清理,而且这个函数是单例的,只处理sync.Pool
类型(你新创建的sync.Pool都会放到一个全局列表中,被这个函数做对象回收)。
不是太容易hack
。