扫描关注获取更多好文
sync.Pool
回顾总结
看文这篇文章,我们来总结一下精华
- sync.Pool是用来缓解频繁的申请内存和缓解因此带来的GC压力的
- 在导入pool包时候init会箱GC注册
poolCleanup
函数,也就是在GC执勤啊运行此函数- P有私有对象池(一个对象位置),公有对象池(多个)
- 优先从自己的本地私有local获取,只有一个,同一时刻P只能运行一个goroutine,速度是最快的
- 如果私有对象没有,那么获取共享池的从头部弹出一个
CompareAndSwapUint64
原子操作的置换- 如果公有池也没有,那么去其他P的shard获取一个
- 最后都没有,那么就只能自己创建一个
- go1.13之后用victim代替mutex锁,GC的时候会直接释放victim,如果victim中对象再次被使用,会返回到存活对象中
- noCopy保证是一个空结构,用来防止pool在第一次使用后被复制
- poolLocal有pad来防止sharding
- 每次GC的将victim设置为空,将原来的local给到victim中
1、false sharding
系统缓存是以行为存储单位的(cache line)为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节,最常见的缓存行大小为64个字节或者128个字节
当系统中
多线程修改相互独立变量时
,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。伪共享是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素,无声的杀手
我们暂且知道这个,后面会用到
2、数据结构
Go 1.13之后
type Pool struct {
noCopy noCopy //noCopy 是一个空结构,用来防止 pool 在第一次使用后被复制
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal,per-P pool, 实际类型为 [P]poolLocal
localSize uintptr // size of the local array,local size
victim unsafe.Pointer // local from previous cycle 上一个周期的本地的待回收对象
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() interface{}
}
// Local per-P Pool appendix.
type poolLocalInternal struct {
private interface{} // Can be used only by the respective P.//私有本地池
shared poolChain // Local P can pushHead/popHead; any P can popTail. 公有池
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 . 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
Go1.13 之前
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 // 池列表
)
以上数据结构我们可以知道
本地池-poolLocal
sync.Pool 为每个P(对应CPU)都分配一个本地池。当进行get或者put的时候,会先将goroutine和某个P的子池关联,在对子池操作,后面我们来验证
本地池分为私有对象和共有对象
私有对象
private interface{}
只能自己的P使用,而且同一时刻P运行的肯定只有一个goroutine,可以看到只能存放一个复用对象公有对象
shared []interface{}
可以被任何的P使用,使用的时候需要加锁poolLocal中的pad
poolLocal中有个pad成员,目的是为了防止false sharding,将剩余的cache line 占用,也可以看出golang的线程cache line是128字节
victim老生代回收站,去除mutex锁
victim有点像java垃圾回收策略的新生代,如果有在被使用的对象,就会放回到local中,如果没有,垃圾回收的时候就会被移除
3、回收逻辑
//此操作在GC的时候出发,直接释放掉victim
func poolCleanup() {
// 直接丢弃当前victim,因为victim没有使用的,是从local拷贝过来的,没有锁操作
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// 将local复制给victim, 并将原local置为nil
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
每次GC的将victim设置为空,将原来的local给到victim中
// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())
在runtime中实现,每次gc的时候都会进行清理和复制,其实也就是清理local的信息,间接证明sync.Pool减小GC压力是避免频繁的申请内存和释放内存
4、Get 源码
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
//将运行的goroutine和P绑定
l, pid := p.pin()
//尝试从私有对象获取,并将私有对象nil
x := l.private
l.private = nil
//如果私有对象为nil,从公有对象池中获取,头部获取
if x == nil {
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
x, _ = l.shared.popHead()
//如果公有池还是没有尝试去其他P的公有池获取一个
if x == nil {
x = p.getSlow(pid)
}
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
//最后无论私有、公有、还是其他共享池都没有,那就只能自己创建一个
if x == nil && p.New != nil {
x = p.New()
}
return x
}
- 优先从自己的本地私有local获取,只有一个,同一时刻P只能运行一个goroutine,速度是最快的
- 如果私有对象没有,那么获取共享池的从头部弹出一个
CompareAndSwapUint64
原子操作的置换- 如果公有池也没有,那么去其他P的shard获取一个
- 最后都没有,那么就只能自己创建一个
去其他池获取过程
func (p *Pool) getSlow(pid int) interface{} {
// See the comment in pin regarding ordering of the loads.
size := atomic.LoadUintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// Try to steal one element from other procs.
//尝试从其他的P中偷取一个
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Try the victim cache. We do this after attempting to steal
// from all primary caches because we want objects in the
// victim cache to age out if at all possible.
//如果获取的还是nil,那么就从victim中试试
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
locals = p.victim
l := indexLocal(locals, pid)
if x := l.private; x != nil {
l.private = nil//优先从vimtic的私有池中获取
return x
}
for i := 0; i < int(size); i++ {//从victim的其他P尝试获取
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// Mark the victim cache as empty for future gets don't bother
// with it.
//如果没有就标记为nil
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
5、Put源码
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
if race.Enabled {
if fastrand()%4 == 0 {
// Randomly drop x on floor.
return
}
race.ReleaseMerge(poolRaceAddr(x))
race.Disable()
}
l, _ := p.pin()
//如果本地队列为nil,设置本地队列
if l.private == nil {
l.private = x
x = nil
}
//如果本地队列不为nil,加入shard中,从头部加入,记得弹出的时候也会从头部弹出
if x != nil {
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
put 的逻辑是优先设置P的本地队列,因为本地私有队列只有一个位置,如果不为空,那么就加入P的共享池中
6、内存泄漏问题
看极客大佬鸟窝的一个案例
var buffers = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func GetBuffer() *bytes.Buffer {
return buffers.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
buffers.Put(buf)
}
案例是很简单,设置buffer,然后返回
这样会有什么问题呢?
没有指定获取和返回的buffer大小,如果put的时候的buffer远远大于初始的buffer大小,且一直增涨的趋势,那么就会造成内存泄漏
最好是指定池子buffer的大小,get的时候根据需要获取对应的池子buffer,put的时候校验buffer的大小,避免内存泄漏
7、内存浪费
还有很多场景是池子的buffer很大,但是我嗯只需要很小的buffer,造成了浪费
我们可以设置多个固定大小的,按需获取
在标准库中net/http/server.go中就存在2k和4k的两个write池子
8、第三方库
bytebufferpool https://github.com/valyala/bytebufferpool
oxtoacart/bpool https://github.com/oxtoacart/bpool