简介

sync.Pool 是 Go 内置的临时对象池函数库,用于缓存临时对象

特点

  • 缓存临时对象

    由于 sync.Pool 会定时清理池中的对象,因此缓存的对象最好为临时对象而不是持久性对象(如DB连接等)

  • 自动扩容、缩容

  • 冗余对象定期释放

    在GC前,sync.Pool 会取消关联池中所有元素,以便其能被gc 回收

  • 多线程安全

一些优化

具体源码解析可参考其他博文,本文只谈一下 sync.Pool 的几个优化点。

lock-free

sync.Pool 使用双向链表存储 shared 对象,该双向链表对于不同的 goroutine 的 push、pop 操作位置不同:

push pop
当前 goroutine 头部 头部
其他 goroutine 尾部

可尽量避免不同 goroutine 对同一队列的锁冲突

同时,针对多个 goroutine,它们各自有各自的 pool,以尽量减少锁冲突

ring buffer

每个双向链表节点(poolChainElt)对应一个环形队列(poolDequeue),环形队列是 ring buffer 的数据结构,是用定长数组实现的环形队列

使用 ring buffer 作为队列,相对于链表来说,对缓存更加友好(因为是连续地址),CPU 可直接在缓存而不是跑到 RAM 中去寻找元素;同时,由于不会删除元素,因此不会给 gc 增加额外的负担。

false sharing

1
2
3
4
5
6
7
type poolLocal struct {
	poolLocalInternal

	// Prevents false sharing on widespread platforms with
	// 128 mod (cache line size) = 0 .
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

CPU 有一二三级缓存,其中一二级缓存是各个核自己独用的,且 CPU 的缓存行是定长的(通常为 64 的倍数)

若有两个核运行的不同 goroutine 访问相邻的 poolLocal,若无 pad 缓冲行对齐填充字段,则 poolLocal 1 可能同时被两个 goroutine 对应的 CPU 核缓存到,那么 goroutine 1 修改 poolLocal 1 会导致 goroutine 2 的 CPU 缓存行失效。因此会造成 CPU 缓存的失效,减慢运行速度。

所以使用 pad 字段,将 poolLocal 结构按照 128 字节长度对齐,避免同一个 poolLocal 被不同 CPU 核缓存。

gc 清理

为了防止大量冗余的临时对象长期存在,浪费内存资源。``sync.Pool使用 runtime_registerPoolCleanup` 函数注册了 gc 钩子,即在 gc 前会触发此钩子函数,以便 gc 清理能池中的临时对象

victim

若 gc 后清空了池中的临时对象,这时用户程序又大量获取临时对象,则会大量创建临时对象,影响程序性能(有点类似于缓存雪崩)

因此 gc 清理临时对象分成两步走。先吧临时对象放到 victim 池中,下一次 gc 再清理,这样使得程序性能更加平滑。

参考资料

https://www.cnblogs.com/cyfonly/p/5800758.html

https://blog.csdn.net/yongjian_lian/article/details/42058893

https://colobu.com/2019/10/08/how-is-sync-Pool-improved-in-Go-1-13/

https://www.cyhone.com/articles/think-in-sync-pool/