Go 源码研究:sync.Pool
sync.Pool 是一个临时对象池。一句话来概括,sync.Pool 管理了一组临时对象,当需要时从池中获取,使用完毕后从再放回池中,以供他人使用。
使用 sync.Pool 只需要编写对象的创建方法:
|
|
因此获取到的对象可能是刚被使用完毕放回池中的对象、亦或者是由 New 创建的新对象。
底层结构
上面已经看到 sync.Pool 内部本质上保存了一个 poolLocal
数组,每个 poolLocal
都只被一个 P 拥有。
|
|
每个 poolLocal
的大小均为缓存行的偶数倍,包含一个 private
私有对象、shared
共享对象 slice
以及一个 Mutex
并发锁。
Get
当从池中获取对象时,会先从 per-P 的 poolLocal
slice 中选取一个 poolLocal
,选择策略遵循:
- 优先从 private 中选择对象
- 若取不到,则对 shared slice 加锁,取最后一个
- 若取不到,则尝试从其他线程中 steal
- 若还是取不到,则使用 New 方法新建
|
|
Put
Put
的过程则相对简单,只需要将对象放回到池中。与取出一样,放回同样拥有策略:
- 优先放入
private
- 如果 private 已经有值,即不能放入,则尝试放入
shared
|
|
细节
上面已经介绍了 Get/Put
的具体策略。我们还有一些细节需要处理:
pin()
pin()
用于取当前 P 中的 poolLocal
。我们来仔细看一下它的实现细节。
|
|
pin()
首先会调用运行时实现获得当前 P 的 id,将 P 设置为禁止抢占。然后检查 pid 与 p.localSize 的值
来确保从 p.local 中取值不会发生越界。如果不会发生,则调用 indexLocal()
完成取值。否则还需要继续调用
pinSlow()
。
|
|
在这个过程中我们可以看到在运行时调整 P 的大小的代价。如果此时 P 被调大,而没有对应的 poolLocal
时,
必须在取之前创建好,从而必须依赖全局加锁,这对于以性能著称的池化概念是比较致命的,因此这也是 pinSlow
的由来。
pinSlow()
因为需要对全局进行加锁,pinSlow()
会首先取消 P 的不可抢占,然后使用 allPoolsMu
进行加锁:
|
|
当完成加锁后,再重新固定 P ,取其 pid。注意,因为中途可能已经被其他的线程调用,因此这时候需要再次对 pid 进行检查。 如果 pid 在 p.local 大小范围内,则不再此时创建,直接返回。
如果 p.local
为空,则将 p 扔给 allPools
并在垃圾回收阶段回收所有 Pool 实例。
最后再完成对 p.local
的创建(彻底丢弃旧数组)
|
|
getSlow()
终于,我们获取到了 poolLocal
。就回到了我们从中取值的过程。在取对象的过程中,我们仍然会面临
既不能从 private 取、也不能从 shared 中取得尴尬境地。这时候就来到了 getSlow()
。
试想,如果我们在本地的 P 中取不到值,是不是可以考虑从别人那里偷一点过来?总会比创建一个新的要快。
因此,我们再次固定 P,并取得当前的 P.id 来从其他 P 中偷值,那么我们需要先获取到其他 P 对应的
poolLocal
。假设 size
为数组的大小,local
为 p.local
,那么尝试遍历其他所有 P:
|
|
我们来证明一下此处确实不会发生取到自身的情况,不妨设:pid = (pid+i+1)%size
则 pid+i+1 = a*size+pid
。
即:a*size = i+1
,其中 a 为整数。由于 i<size
,于是 a*size = i+1 < size+1
,则:
(a-1)*size < 1
==> size < 1 / (a-1)
,由于 size
为非负整数,这是不可能的。
因此当取到其他 poolLocal
时,便能从 shared 中取对象了。
|
|
运行时垃圾回收
sync.Pool 的垃圾回收发生在运行时 GC 开始之前。
src/sync/pool.go
:
|
|
src/runtime/mgc.go
:
|
|
再来看实际的清理函数:
|
|
noCopy
noCopy 是 go1.7 开始引入的一个静态检查机制。它不仅仅工作在运行时或标准库,同时也对用户代码有效。 用户只需实现这样的不消耗内存、仅用于静态分析的结构来保证一个对象在第一次使用后不会发生复制。
|
|
总结
至此,我们完整分析了 sync.Pool 的所有代码。总结:
|
|
一个 goroutine 固定在 P 上,从当前 P 对应的 poolLocal
取值,
若取不到,则从对应的 shared 上取,若还是取不到,则尝试从其他 P 的 shared 中偷。
若偷不到,则调用 New 创建一个新的对象。池中所有临时对象在一次 GC 后会被全部清空。
对于调用方而言,当 Get 到临时对象后,便脱离了池本身不受控制。 用方有责任将使用完的对象放回池中。