Go sync.Pool 解析
Contents
简介
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
|
|
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/
Author Jakseer
LastMod 2022-02-12