最近在工作中遇到了 sync.Pool
,当时看的一知半解,大致觉得是一个内存池来的,后面想仔细看一下sync.Pool
是怎么使用的,但是一看源码就懵逼了,基本看不懂逻辑。因为sync.Pool
的源码涉及到 go 的调度的知识,如果不清楚 go 的调度,看不懂是正常的。所以建议先去了解一下 go 的调度
是什么
pool
如果从名字上看,就是池的意思,但是用池来形容不够恰当,应该称为cache
,因为 Pool
里装的对象可以被无通知地被回收,所以 pool
不适合用来当 socket 连接池
有什么用
首先看源码的注释
// Pool's purpose is to cache allocated but unused items for later reuse,
// relieving pressure on the garbage collector. That is, it makes it easy to
// build efficient, thread-safe free lists. However, it is not suitable for all
// free lists.
对于很多需要重复分配,回收内存的地方,sync.Pool
是个很好的解决方案,因为频繁地分配、回收内存会给 GC 带来一定的压力,而 sync.Pool
可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。
怎么用
先来看个例子
package main
import(
"fmt"
"sync"
)
func main() {
// 创建一个缓存int对象的一个pool
p := &sync.Pool{
New: func() interface{} {
return 0
},
}
a := p.Get().(int)
p.Put(1)
b := p.Get().(int)
fmt.Println(a, b)
}
输出的结果是 0 1
,创建的时候可以指定一个New函数,获取对象的时候如何在池里面找不到缓存的对象将会使用指定的 new 函数创建一个返回,如果没有new函数则返回nil
,使用的方法很简单。
阅读源码的注释之后,有几个点需要关注的:
-
get()
的数据和put()
进去的数据是没有关系的 -
pool
是线程安全的 -
缓存池的大小是不受限制的(受限于内存)
-
pool
包在初始化的时候注册了poolCleanup
函数,它会清除所有的 pool 里面的所有缓存的对象,该函数注册进去之后会在每次 gc 之前都会调用,因此sync.Pool
缓存的期限只是两次 gc 之间这段时间func init() { runtime_registerPoolCleanup(poolCleanup) }
缓存对象的开销
如何在多个 goroutine
之间使用同一个 pool 做到高效呢?官方的做法就是尽量减少竞争,因为 sync.pool
为每个P都分配了一个子池。如下所示
当执行 put
和 get
操作的时候,都会把 goroutine
固定在某个 P 的子池上,然后对其进行操作。私有对象只有当前 P 使用,不用加锁,共享列表对象的是与其他 P 分享的,所以需要加锁
get的过程:
- 固定在某个
P
的子池上,尝试从私有对象获取,如果私有对象非空则返回获取的值,然后把私有对象置空 - 如果私有对象为空,则去共享对象列表获取(需要加锁)
- 如果共享对象列表为空,那么去其他
P
的共享对象列表偷一个(需要加锁) - 如果其他
P
的共享对象列表也为空,那么就new
一个出来
可以看到一次 get 操作最少 0 次加锁,最大 N(N等于MAXPROCS)次加锁。
put 的过程
-
固定到某个 P,如果私有对象为空则放到私有对象;
-
否则加入到该 P 子池的共享列表中(需要加锁)。
可以看到一次put操作最少0次加锁,最多1次加锁。
总的来说,pool 虽然减少了 gc 的负担,但是其开销也是不小的