Go语言sync.Pool的用法(附带实例)
在 sync 包中,有一个名为 sync.Pool 的结构体,它是不是 Go 语言原生提供的对象池呢?答案是否定的,我们不要被它的名字所迷惑。
sync.Pool 的作用是缓存对象,增加对象被重用的概率,减少垃圾回收的负担,但它并不适合用作连接池,可能“sync.Cache”这个名字更适合它。
为什么这样说呢?我们先来看看 sync.Pool 是怎么使用的,示例代码如下:
v,_ := pool.Get().(time.Time) 有以下 3 层含义:
紧接着的代码定义了 time.Time 类型的变量 i,这样前面的变量 v 就可以赋值给这个变量 i 了。另外,如果前面的变量 v 没有做类型断言,也可以直接使用短变量赋值的方式让 Go 语言自动推断类型。使用 fmt.Printf("%T",i) 可以看到变量 i 的类型是 time.Time。
此示例的输出看起来与之前定义的对象池类似!不过,两者其实是有区别的,它们的区别在于生命周期不同!
输出结果如下:
sync.Pool 清理缓存对象的关键代码如下:
在 init() 函数调用这个函数时,传入的参数就是用于清理对象的 poolCleanup() 函数。如果 sync.Pool 中的对象超过一定时间没有被访问,runtime 就会调用这个清理函数将 sync.Pool 中缓存的所有对象清理掉。poolCleanup 这个清理函数是在 runtime 内部执行的,不受应用程序的控制。
poolCleanup() 函数会在每次进行垃圾回收前被调用。因此 sync.Pool 中缓存对象的生命周期是两次垃圾回收间隔的时间。因为垃圾回收具有不确定性,所以我们无法精确地控制 sync.Pool 中缓存对象的生命周期,可以说池中的对象是临时的,而这违反了创建池的初衷,因此不能使用 sync.Pool 作为数据对象池。
另外,由于在创建 sync.Pool 时不能指定大小,因此缓存对象的数量也不受控制,数量的多少仅受限于宿主机内存的大小,使用 sync.Pool 是无法控制缓存对象的数量的。
使用 sync.Pool 需要注意的是,若被放入池中的对象在池中被重用,其状态有可能是发生了改变的,如果再次使用这个对象时没有对其状态进行重置,可能会导致程序出现错误。
sync.Pool 的使用场景有以下几个:
sync.Pool 的定位不是做类似对象池这样的组件,考虑到垃圾回收的特性,它也不适合做对象池。它的作用是增加对象重用的概率,降低复杂对象的创建频率,减少垃圾回收的负担。另外,sync.Pool 是线程安全的,会有锁的开销,多个协程可以并发地调用它的方法存取对象。
在 Go1.13 版本之前,sync.Pool 存在以下两个问题:
在 Go1.13 版本之后,sync.Pool 针对以上两个问题做了优化,在此不再赘述。
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 层含义:
- 调用 pool.Get() 函数,从池中获取一个元素。池是一个元素可以重用的对象的集合,Get() 方法会从池中获取一个元素,如果池为空,则会创建一个新元素;
- pool.Get().(time.Time) 对获取到的元素做了类型断言,将其转换为了 time.Time 类型,并将其赋给了变量 v;
- 使用空标识符_丢弃了类型断言的第二个返回值,因为这个值表示类型断言是否成功,在这儿我们只需要获取转换后的 time.Time 类型的值即可,不需要关心类型断言是否成功。
紧接着的代码定义了 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)代码说明如下:
- pool.Put(1) 中的 Put(1) 其实是 Put(interface{}),所以编译和运行时不会报错。在 fmt.Printf ("%T=%v\n", v1, v1) 处查看变量 v1 的类型和值,验证了 v1 := pool.Get() 返回的是 int 类型的值。
- 代码 pool.Put(2) 表示向对象池中放入值 2,紧接着调用 runtime.GC,手动触发垃圾回收。
- 执行 v2 := pool.Get() 获取值,期望 fmt.Println(pool.Get()) 输出 2。但是实际上,在执行了 runtime.GC 后再次调用 Get() 方法获取对象值时,会发现 pool 是空的。在这种情况下,会去调用 New 字段对应的匿名函数重新创建一个对象,因此会输出“创建新的对象!”。而输出结果也验证了新创建的对象又变成了 time.Time 类型,而非之前的 2。
输出结果如下:
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 清理缓存对象的关键代码如下:
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 来缓存 net.Conn 对象(临时的中间结果)等;
- 需要频繁地创建和销毁大对象,例如图片、视频等。
sync.Pool 的定位不是做类似对象池这样的组件,考虑到垃圾回收的特性,它也不适合做对象池。它的作用是增加对象重用的概率,降低复杂对象的创建频率,减少垃圾回收的负担。另外,sync.Pool 是线程安全的,会有锁的开销,多个协程可以并发地调用它的方法存取对象。
在 Go1.13 版本之前,sync.Pool 存在以下两个问题:
- 因为每次垃圾回收都会触发回收临时缓存对象的机制,所以当临时缓存对象的数量过多时,就会导致 STW 变长。并且,如果所有的缓存对象都被回收,那么调用Get方法时的命中率就会下降,不得不基于缓存机制重新创建许多新对象;
- 在 sync.Pool 的底层实现上使用了互斥锁,如果对这个锁的并发请求竞争激烈,则会导致性能下降。
在 Go1.13 版本之后,sync.Pool 针对以上两个问题做了优化,在此不再赘述。