golang新版如何优化sync.pool锁竞争消耗?

前言:

sync.pool是golang的标准库,通过堆对象复用达到减少gc延迟的库。相比不断的创建堆对象,sync.pool通过对象复用确实可以减少gc的延迟。但sync.pool也是有损耗的,损耗主要体现在锁竞争上。go1.13版在sync.pool下了不少功夫来优化锁。

commit:
https://github.com/golang/go/commit/d5fd2dd6a17a816b7dfd99d4df70a85f1bf0de31#diff-491b0013c82345bf6cfa937bd78b690d

代码分析:

这里简单说下sync.pool的逻辑,每个p都有独享的缓存队列,当g进行sync.pool操作时,先找到所属p的private,如果没有对象可用,加锁从 shared切片里获取数据,如果还没有数据,那么到别的poolLocal进行偷数据,如果偷不到,那么就创建新对象。

该文章后续仍在不断的更新修改中, 请移步到原文地址  http://xiaorui.cc/?p=5878

// xiaorui.cc

type Pool struct {  
    local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal  
    localSize uintptr        // size of the local array  
   
    New func() interface{}  
}  
  
// 每个p有一个poolLocal 
// private存一个缓存对象,shared不仅自己用,而且别的p也会过来偷。
type poolLocal struct {  
    private interface{}   // Can be used only by the respective P.  
    shared  []interface{} // Can be used by any P.  
    Mutex                 // 保护shared缓存  
    pad     [128]byte     // Prevents false sharing.  
}  

func (p *Pool) Put(x interface{}) {
    if x == nil {
        return
    }

    l := p.pin() // 获取关联的poolLocal,另外间接的保护了private对象
    if l.private == nil {
        l.private = x
        x = nil
    }

    runtime_procUnpin()
    if x != nil {
        l.Lock() // 保护shared
        l.shared = append(l.shared, x)
        l.Unlock()
    }
}

func (p *Pool) Get() interface{} {  
    if raceenabled {  
        if p.New != nil {  
            return p.New()  
        }  
        return nil  
    }  
    l := p.pin()   // 获取g所属p的poolLocal,并锁定
    x := l.private // 判断private是否有缓存
    l.private = nil  
    runtime_procUnpin()  
    if x != nil {  
        return x  
    }  
    l.Lock()  // private没有缓存对象,就尝试从p所属的shared尾部拿缓存
    last := len(l.shared) - 1  
    if last >= 0 {  
        x = l.shared[last]  
        l.shared = l.shared[:last]  
    }  
    l.Unlock()  
    if x != nil {  
        return x  
    }  
    return p.getSlow()  // 偷别人的缓存对象
}  

func (p *Pool) getSlow() (x interface{}) {   
    size := atomic.LoadUintptr(&p.localSize) // load-acquire  
    local := p.local                         // load-consume  
    pid := runtime_procPin()  
    runtime_procUnpin()  

    // 从其他p里进行偷缓存对象
    for i := 0; i < int(size); i++ {
        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()  // 方锁
    }  

    // 创建一个新的对象
    if x == nil && p.New != nil {  
        x = p.New()  
    }  
    return x  
} 

我们通过分析1.12 sync.pool的源码,可以发现sync.pool里会有各种的锁逻辑,从自己的shared拿数据,加锁。getSlow偷缓存,也是需要给每个p加锁。put归还缓存的时候,还是会mutex加一次锁。

go mutex锁的实现原理,我在以前的文章中说过好几次了。简单说,他开始也是atomic cas自旋,默认是4次尝试,当还没有拿到锁的时候,会进行 waitqueue gopack休眠调度处理。
但这里你以为race中的g可以顺利的陷入wait queue里?不,lock_futex.go的futexsleep逻辑会加大你的锁竞争消耗。可以通过strace来追一下系统调用。

go在1.13的版本中优化了sync.pool的锁竞争问题,这里还改变了shared的数据结构,以前的版本用切片做缓存,现在换成了poolChain双端链表。这个双端链表的设计很有意思,你看sync.pool源代码会发现跟redis quicklist相似,都是链表加数组的设计。

// xiaorui.cc
type poolChain struct {
	head *poolChainElt

	tail *poolChainElt
}

type poolChainElt struct {
	poolDequeue

	next, prev *poolChainElt
}

type poolDequeue struct {
	headTail uint64

	vals []eface
}

type eface struct {
	typ, val unsafe.Pointer
}

下面是go sync.pool get和put的锁优化实现,以前不管是获取本地的shared,还是偷其他p的shared,过程都需要加锁的。新的sync.pool是通过atomic规避锁的竞争。


// xiaorui.cc
type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array
	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
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

func (p *Pool) Put(x interface{}) {
	if x == nil {
		return
	}
        . . .
	l, _ := p.pin()  // 拿到g所属p的poolLocal, 并锁定

        // 相比1.12版本,取消了 mutex 调用.
	if l.private == nil {
		l.private = x
		x = nil
	}
	if x != nil {
                // 归还缓存
		l.shared.pushHead(x)
	}
	runtime_procUnpin()
	if race.Enabled {
		race.Enable()
	}
}

func (p *Pool) Get() interface{} {
	if race.Enabled {
		race.Disable()
	}
	l, pid := p.pin()
	x := l.private
	l.private = nil
	if x == nil {
		x, _ = l.shared.popHead()  // 从本地的shared拿缓存
		if x == nil {
			x = p.getSlow(pid) // 尝试偷缓存
		}
	}
	runtime_procUnpin()
	if x == nil && p.New != nil {
		x = p.New() // 没偷到缓存,先建缓存
	}
	return x
}

func (p *Pool) getSlow(pid int) interface{} {
	size := atomic.LoadUintptr(&p.localSize) // load-acquire
	locals := p.local                        // load-consume
	// Try to steal one element from other procs.
	for i := 0; i < int(size); i++ {
		l := indexLocal(locals, (pid+i+1)%int(size))
		if x, _ := l.shared.popTail(); x != nil {
			return x
		}
	}
        . . .
}

func (c *poolChain) popTail() (interface{}, bool) {
	d := loadPoolChainElt(&c.tail)
	if d == nil {
		return nil, false
	}
	for {
		d2 := loadPoolChainElt(&d.next)
		if val, ok := d.popTail(); ok {
			return val, ok
		}

		if d2 == nil {
			return nil, false
		}

                // 恩,使用cas来替换tail
		if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.tail)), unsafe.Pointer(d), unsafe.Pointer(d2)) {
			storePoolChainElt(&d2.prev, nil)
		}
		d = d2
	}
}

sync.pool是否存在死锁问题,或者说怎么规避的死锁?

golang1.12版在private读取使用了runtime pin锁,本地shared队列使用了poolLocal的mutex锁,向其他p偷缓存时迭代每个p的shared队列,每次的迭代为了安全当然也会加锁。
那么我们可以想一下这样是否有死锁的风险? 没有,我们可以从get方法跟下去,每个过程都会放锁,锁的粒度虽然很细,但释放及时。另外偷任务是顺序加锁的过程,更不会存在死锁。
1.13版本是不管put和get都有一个大的runtime pin,后面都是atomic的操作了。

总结:

sync.pool是优化gc的利器,但也不是多多益善,还是需要用测试的数据来说话。
我的一个小经验,只要看到pprof off-cpu火焰图和strace pselect6异常,那么可以考虑优化锁竞争了。
另外文章里我省略了 victim gc的优化,毕竟在1.12版本里已经有了 victim的gc优化。


对Golang感兴趣的朋友可以加群: 278517979 !!!
另外如果大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码, 感谢!
另外再次标注博客原地址  xiaorui.cc