IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    类型安全的Pool

    smallnest发表于 2024-04-10 00:25:11
    love 0

    池(sync.Pool)是一组可单独保存(Set)和检索(Get)的临时对象集合。

    存储在池中的任何项都可能在任何时候自动移除而无需通知。如果池在移除项时持有该对象的唯一引用,那么这个对象可能会被释放掉。

    池能够确保在多个goroutine同时访问时的安全性。

    池的目的在于缓存已分配但未使用的对象以便后续复用,减轻垃圾收集器的压力。

    也就是说池的功能是为了重用对象,目的是减轻GC的压力。

    类型不安全?

    你看sync.Pool提供的方法:

    1
    2
    3
    4
    5
    6
    type Pool struct {
    New func() any
    }
    func (p *Pool) Get() any
    func (p *Pool) Put(x any)

    它存储的对象类型是any,这样的话,我们在使用的时候就需要进行类型转换,这样就会导致类型不安全,或者说使用起来很麻烦。

    比就以官方的例子为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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,通过巧妙的包装,简单几行代码就实现了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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方法在把接口类型转换为泛型类型时,为什么不需要进行错误检查呢:

    1
    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自己修改呢?难度很大,主要在于下面一点:

    1
    2
    3
    4
    5
    6
    func init() {
    runtime_registerPoolCleanup(poolCleanup)
    }
    // Implemented in runtime.
    func runtime_registerPoolCleanup(cleanup func())

    sync.Pool在运行时中插入了一个桩子,运行时在垃圾回收的时候,会调用函数做对象的清理,而且这个函数是单例的,只处理sync.Pool类型(你新创建的sync.Pool都会放到一个全局列表中,被这个函数做对象回收)。

    不是太容易hack。



沪ICP备19023445号-2号
友情链接