总结
- pool 是用来缓解 Gc 压力的,会在每次 Gc 之前全部清空 pool,GC 默认每两分钟一次
- 在倒入 pool 包时执行的 init 函数会向 GC 注册
poolCleanup
函数,也就是在 GC 之前会运行该函数。 - pool 也存在私有队列为空的时候,从全局队列偷取一部分
- noCopy 保证是一个空结构,用来防止 pool 在第一次使用后被复制
- 分为本地 local 和 global 队列 来绑定 P 进行操作
- poollocal 有 pad 来防止
false sharding
流程图
为了使得在多个 goroutine 中高效的使用 goroutine,sync.Pool 为每个 P(对应 CPU) 都分配一个本地池,当执行 Get 或者 Put 操作的时候,会先将 goroutine 和某个 P 的子池关联,再对该子池进行操作。 每个 P 的子池分为私有对象和共享列表对象,私有对象只能被特定的 P 访问,共享列表对象可以被任何 P 访问。因为同一时刻一个 P 只能执行一个 goroutine,所以无需加锁,但是对共享列表对象进行操作时,因为可能有多个 goroutine 同时操作,所以需要加锁。
值得注意的是 poolLocal 结构体中有个 pad 成员,目的是为了防止 false sharing。cache 使用中常见的一个问题是 false sharing。当不同的线程同时读写同一 cache line 上不同数据时就可能发生 false sharing。false sharing 会导致多核处理器上严重的系统性能下降。具体的可以参考伪共享 (False Sharing)。
false sharding
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64 个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局
数据结构
type Pool struct {
noCopy noCopy // noCopy 是一个空结构,用来防止 pool 在第一次使用后被复制
local unsafe.Pointer // per-P pool, 实际类型为 [P]poolLocal
localSize uintptr // local 的 size
// New 在 pool 中没有获取到,调用该方法生成一个变量
New func() interface{}
}
// 具体存储结构
type poolLocalInternal struct {
private interface{} // 只能由自己的 P 使用
shared []interface{} // 可以被任何的 P 使用
Mutex // 保护 shared 线程安全
}
type poolLocal struct {
poolLocalInternal
// 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
var (
allPoolsMu Mutex
allPools []*Pool // 池列表
)
- noCopy 保证是一个空结构,用来防止 pool 在第一次使用后被复制
- 分为本地 local 和 global 队列 来绑定 P 进行操作
- poollocal 有 pad 来防止
false sharding
主体流程
Put 方法
Put 方法的整个流程比较简单,主要是将用完的对象放回池中,看一**释就可以理解。
- 获取当前私有队列,放入私有本地队列失败,放入全局队列
func (p *Pool) Put(x interface{}) {
...
// 获取当前 P 的 pool
l := p.pin()
// 私有属性为空 放入
if l.private == nil {
l.private = x
x = nil
}
runtime_procUnpin()
// 私有属性放入失败 放入 shared 池
if x != nil {
l.Lock()
l.shared = append(l.shared, x)
l.Unlock()
}
...
}
Get 方法
我们找到对应的代码如下,
func (p *Pool) Get() interface{} {
...
// 获取当前 P 的 poolLocal
l := p.pin()
// 先从 private 读取
x := l.private
l.private = nil
runtime_procUnpin()
// private 没有
if x == nil {
l.Lock()
// 从当前 P 的 shared 末尾取一个
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
}
l.Unlock()
// 还没有取到 则去其他 P 的 shared 取
if x == nil {
x = p.getSlow()
}
}
...
// 最后还没取到 调用 NEW 方法生成一个
if x == nil && p.New != nil {
x = p.New()
}
return x
}
上面有一个 p.getSlow()
操作是说从其他的 P 中偷取一个,比较有意思,在 Go 的 GMP 模型中也存在这个偷的概念,基本和这个类似。我们来看看
func (p *Pool) getSlow() (x interface{}) {
...
// 尝试从其他 P 中窃取一个元素。
pid := runtime_procPin()
runtime_procUnpin()
for i := 0; i < int(size); i++ {
// 获取其他 P 的 poolLocal
l := indexLocal(local, (pid+i+1)%int(size))
l.Lock()
last := len(l.shared) - 1
if last >= 0 {
x = l.shared[last]
l.shared = l.shared[:last]
l.Unlock()
break
}
l.Unlock()
}
return x
}
存活周期以及内存回收
在倒入 pool 包时执行的 init 函数会向 GC 注册 poolCleanup
函数,也就是在 GC 之前会运行该函数。
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
我们来看看 poolCleanup,该函数主要是将所有池的变量解除引用,为下一步的 GC 作准备。
func poolCleanup() {
// 在 GC 时会调用此函数。
// 它不能分配,也不应该调用任何运行时函数。
// 防御性地将所有东西归零,原因有两个:
// 1. 防止整个池的错误保留。
// 2. 如果GC发生时goroutine与Put / Get中的l.shared一起使用,它将保留整个Pool。因此下一周期内存消耗将增加一倍。
for i, p := range allPools {
// 将所有池对象接触引用 等待 GC 回收
allPools[i] = nil
for i := 0; i < int(p.localSize); i++ {
l := indexLocal(p.local, i)
l.private = nil
for j := range l.shared {
l.shared[j] = nil
}
l.shared = nil
}
p.local = nil
p.localSize = 0
}
allPools = []*Pool{}
}