golang原始碼分析:sync.Pool 如何從讀寫加鎖到無鎖

MoeYang發表於2020-12-14

我們知道,早期的sync.Pool,底層是通過互斥鎖實現對共享佇列的併發訪問的,這裡會存在的問題是,儘管是分段鎖,高併發場景下頻繁進行G的wait和runable狀態排程其實開銷也不算小

 通過互斥鎖保證併發安全的sync.Pool資料結構

type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local,固定大小per-P池, 實際型別為 [P]poolLocal
	localSize uintptr        // local array 的大小
	//  New 方法在 Get 失敗的情況下,選擇性的建立一個值, 否則返回nil
	New func() interface{}
}

type poolLocal struct {
	poolLocalInternal

	// 將 poolLocal 補齊至兩個快取行的倍數,防止 false sharing,
	// 每個快取行具有 64 bytes,即 512 bit
	// 目前我們的處理器一般擁有 32 * 1024 / 64 = 512 條快取行
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

// Local per-P Pool appendix.
type poolLocalInternal struct {
	private interface{}   // 只能被區域性排程器P使用
	shared  []interface{} // 所有P共享
	Mutex                 // 讀寫shared前要加鎖
}

通過雙向連結串列實現的無鎖sync.Pool資料結構

local和victim都指向poolLocal陣列,它們的區別是: pool裡面的兩個poolLocal陣列,每次經歷STW後victim置零,將local賦值給victim,local置零。

這裡的allPools和oldPools都儲存的Pool集合。allPools儲存的是未經歷STW的Pool,使用的是pool的local欄位;allPools經歷STW之後將local變成victim,allPools變成oldPools。這兩個變數不參與存取過程,僅在STW的時候使用。

poolCleanup

func init() {

runtime_registerPoolCleanup(poolCleanup)

}

sync.Pool在init()中向runtime註冊了一個cleanup方法,它在STW1階段被呼叫的

也就是說,pool中快取的物件,最多生存2次GC就會完全回收

資料結構

我們看一下這裡put和get的方法

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

    // 這裡取消了 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
		}

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

為什麼這裡用cas替換了Mutex

大家知道cas的機制是自旋重試,它適用的場景是鎖粒度小、釋放快的場景,這樣cas快速重試幾次就能成功

而互斥鎖會導致協程gopark\進入waitqueue,釋放鎖後又被goready叫醒再被排程

對於pool的場景下,本身是鎖粒度小、釋放快的場景,相比互斥鎖,cas大概率原地重試幾下就能拿到鎖,相比切換協程狀態重新排程,效能開銷就會低一些。

相關文章