首页 > 编程笔记 > Go语言笔记 阅读:14

Go语言sync.Pool的用法(附带实例)

在 sync 包中,有一个名为 sync.Pool 的结构体,它是不是 Go 语言原生提供的对象池呢?答案是否定的,我们不要被它的名字所迷惑。

sync.Pool 的作用是缓存对象,增加对象被重用的概率,减少垃圾回收的负担,但它并不适合用作连接池,可能“sync.Cache”这个名字更适合它。

为什么这样说呢?我们先来看看 sync.Pool 是怎么使用的,示例代码如下:
// 测试sync.Pool的使用
func TestSyncPool(t *testing.T) {
     //创建一个 &sync.Pool,以匿名函数返回空接口的形式返回time.Time类型的now()
     //为了方便演示,每当这个pool对象调用New方法时就输出一个创建的信息
     //如果使用了缓存,则不会输出"创建新的对象!"
     pool := &sync.Pool{
             New: func() interface{} {
                     fmt.Println("创建新的对象!")
                     return time.Now()
             },
     }
     //因为返回的是空接口类型,所以要先做类型断言
     v,_ := pool.Get().(time.Time)

     //定义一个time.Time类型的变量i,将v的值赋予这个i
     //如果没有上一句代码做类型断言,在此处就会报错
     //这是因为Go语言不支持隐式转换,i是时间类型time.Time,v是接口类型interface
     //两种不同的类型不能直接赋值,直接赋值就会报错
     var i time.Time
     i = v
     fmt.Println(i) //输出:time.Time=2021-01-06 16:55:05.3693992 +0800 CST m=+0.014996101
    //next runtime.GC()
}
上述代码中创建了一个名为 sync.Pool 的指针对象,其中 New 这段代码是一个匿名函数,当 pool 为空时,就会调用这个匿名函数创建一个 time.Now() 方法并返回。如果 pool 不为空,则可以直接使用。

v,_ := pool.Get().(time.Time) 有以下 3 层含义:
紧接着的代码定义了 time.Time 类型的变量 i,这样前面的变量 v 就可以赋值给这个变量 i 了。另外,如果前面的变量 v 没有做类型断言,也可以直接使用短变量赋值的方式让 Go 语言自动推断类型。使用 fmt.Printf("%T",i) 可以看到变量 i 的类型是 time.Time。

此示例的输出看起来与之前定义的对象池类似!不过,两者其实是有区别的,它们的区别在于生命周期不同!

sync.Pool中对象的生命周期

接下来看看存储在 sync.Pool 中的对象的生命周期。将下面这段代码加入前面示例代码 //next runtime.GC() 的后面。
pool.Put(1)
v1 := pool.Get()
fmt.Printf("%T=%v\n", v1, v1)

pool.Put(2)
runtime.GC()

v2 := pool.Get()
fmt.Printf("%T=%v\n", v2, v2)
代码说明如下:
输出结果如下:

int=1
创建新的对象!
2021-01-06 16:55:05.4684553 +0800 CST m=+0.114052201
time.Time=2021-01-06 16:55:05.4684553 +0800 CST m=+0.114052201

上面的代码演示了垃圾回收会触发清除 sync.Pool 中缓存的对象的机制,缓存对象的生命周期是从这次创建开始到下一次进行垃圾回收时结束。

sync.Pool 清理缓存对象的关键代码如下:
func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

// 在运行时实现
func runtime_registerPoolCleanup(cleanup func())

//清理缓存对象
func poolCleanup() {
    ...
}
可以看到 sync.Pool 中的 init() 函数调用了 runtime_registerPoolCleanup() 函数,而 runtime_registerPoolCleanup() 是 runtime 提供的函数,用于注册清理函数。

在 init() 函数调用这个函数时,传入的参数就是用于清理对象的 poolCleanup() 函数。如果 sync.Pool 中的对象超过一定时间没有被访问,runtime 就会调用这个清理函数将 sync.Pool 中缓存的所有对象清理掉。poolCleanup 这个清理函数是在 runtime 内部执行的,不受应用程序的控制。

poolCleanup() 函数会在每次进行垃圾回收前被调用。因此 sync.Pool 中缓存对象的生命周期是两次垃圾回收间隔的时间。因为垃圾回收具有不确定性,所以我们无法精确地控制 sync.Pool 中缓存对象的生命周期,可以说池中的对象是临时的,而这违反了创建池的初衷,因此不能使用 sync.Pool 作为数据对象池。

另外,由于在创建 sync.Pool 时不能指定大小,因此缓存对象的数量也不受控制,数量的多少仅受限于宿主机内存的大小,使用 sync.Pool 是无法控制缓存对象的数量的。

sync.Pool的使用场景

因为 Go 语言为 sync.Pool 设置了回收机制,所以使用 sync.Pool 时不用担心它会一直增长。

使用 sync.Pool 需要注意的是,若被放入池中的对象在池中被重用,其状态有可能是发生了改变的,如果再次使用这个对象时没有对其状态进行重置,可能会导致程序出现错误。

sync.Pool 的使用场景有以下几个:
sync.Pool 的定位不是做类似对象池这样的组件,考虑到垃圾回收的特性,它也不适合做对象池。它的作用是增加对象重用的概率,降低复杂对象的创建频率,减少垃圾回收的负担。另外,sync.Pool 是线程安全的,会有锁的开销,多个协程可以并发地调用它的方法存取对象。

在 Go1.13 版本之前,sync.Pool 存在以下两个问题:
在 Go1.13 版本之后,sync.Pool 针对以上两个问题做了优化,在此不再赘述。

相关文章