深入淺出Golang中的sync.Pool
學(xué)習(xí)到的內(nèi)容:
1.一個(gè)64位的int類型值,充分利用高32位和低32位,進(jìn)行相關(guān)加減以及從一個(gè)64位中拆出高32位和低32位.
擴(kuò)展:如何自己實(shí)現(xiàn)一個(gè)無鎖隊(duì)列.
- 如何判斷隊(duì)列是否滿.
- 如何實(shí)現(xiàn)無鎖化.
- 優(yōu)化方面需要思考的東西.
2.內(nèi)存相關(guān)操作以及優(yōu)化
- 內(nèi)存對齊
- CPU Cache Line
- 直接操作內(nèi)存.
一、原理分析
1.1 結(jié)構(gòu)依賴關(guān)系圖

下面是相關(guān)源代碼,不過是已經(jīng)刪減了對本次分析沒有用的代碼.
type Pool struct {
// GMP中,每一個(gè)P(協(xié)程調(diào)度器)會有一個(gè)數(shù)組,數(shù)組大小位localSize.
local unsafe.Pointer
// p 數(shù)組大小.
localSize uintptr
New func() any
}
// poolLocal 每個(gè)P(協(xié)程調(diào)度器)的本地pool.
type poolLocal struct {
poolLocalInternal
// 保證一個(gè)poolLocal占用一個(gè)緩存行
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
type poolLocalInternal struct {
private any // Can be used only by the respective P. 16
shared poolChain // Local P can pushHead/popHead; any P can popTail. 8
}
type poolChain struct {
head *poolChainElt
tail *poolChainElt
}
type poolChainElt struct {
poolDequeue
next, prev *poolChainElt
}
type poolDequeue struct {
// head 高32位,tail低32位.
headTail uint64
vals []eface
}
// 存儲具體的value.
type eface struct {
typ, val unsafe.Pointer
}1.2 用圖讓代碼說話

1.3 Put過程分析
Put 過程分析比較重要,因?yàn)檫@里會包含pool所有依賴相關(guān)分析.
總的分析學(xué)習(xí)過程可以分為下面幾個(gè)步驟:
1.獲取P對應(yīng)的poolLocal
2.val如何進(jìn)入poolLocal下面的poolDequeue隊(duì)列中的.
3.如果當(dāng)前協(xié)程獲取到當(dāng)前P對應(yīng)的poolLocal之后進(jìn)行put前,協(xié)程讓出CPU使用權(quán),再次調(diào)度過來之后,會發(fā)生什么?
4.讀寫內(nèi)存優(yōu)化.
數(shù)組直接操作內(nèi)存,而不經(jīng)過Golang
充分利用uint64值的特性,將head和tail用一個(gè)值來進(jìn)行表示,減少CPU訪問內(nèi)存次數(shù).
獲取P對應(yīng)的poolLocal
sync.Pool.local其實(shí)是一個(gè)指針,并且通過變量+結(jié)構(gòu)體大小來劃分內(nèi)存空間,從而將這片內(nèi)存直接劃分為數(shù)組. Go 在Put之前會先對當(dāng)前Goroutine綁定到當(dāng)前P中,然后通過pid獲取其在local內(nèi)存地址中的歧視指針,在獲取時(shí)是會進(jìn)行內(nèi)存分配的. 具體如下:
func (p *Pool) pin() (*poolLocal, int) {
// 返回運(yùn)行當(dāng)前協(xié)程的P(協(xié)程調(diào)度器),并且設(shè)置禁止搶占.
pid := runtime_procPin()
s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
l := p.local // load-consume
// pid < 核心數(shù). 默認(rèn)走該邏輯.
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
// 設(shè)置的P大于本機(jī)CPU核心數(shù).
return p.pinSlow()
}
// indexLocal 獲取當(dāng)前P的poolLocal指針.
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
// l p.local指針開始位置.
// 我猜測這里如果l為空,編譯階段會進(jìn)行優(yōu)化.
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
// uintptr真實(shí)的指針.
// unsafe.Pointer Go對指針的封裝: 用于指針和結(jié)構(gòu)體互相轉(zhuǎn)化.
return (*poolLocal)(lp)
}從上面代碼我們可以看到,Go通過runtime_procPin來設(shè)置當(dāng)前Goroutine獨(dú)占P,并且直接通過頭指針+偏移量(數(shù)組結(jié)構(gòu)體大小)來進(jìn)行對內(nèi)存劃分為數(shù)組.
Put 進(jìn)入poolDequeue隊(duì)列:
Go在Push時(shí),會通過headtail來獲取當(dāng)前隊(duì)列內(nèi)元素個(gè)數(shù),如果滿了,則會重新構(gòu)建一個(gè)環(huán)型隊(duì)列poolChainElt,并且設(shè)置為poolChain.head,并且賦值next以及prev.
通過下面代碼,我們可以看到,Go通過邏輯運(yùn)算判斷隊(duì)列是否滿的設(shè)計(jì)時(shí)非常巧妙的,如果后續(xù)我們?nèi)ラ_發(fā)組件,也是可以這么進(jìn)行設(shè)計(jì)的。
func (c *poolChain) pushHead(val any) {
d := c.head
// 初始化.
if d == nil {
// Initialize the chain.
const initSize = 8 // Must be a power of 2
d = new(poolChainElt)
d.vals = make([]eface, initSize)
c.head = d
// 將新構(gòu)建的d賦值給tail.
storePoolChainElt(&c.tail, d)
}
// 入隊(duì).
if d.pushHead(val) {
return
}
// 隊(duì)列滿了.
newSize := len(d.vals) * 2
if newSize >= dequeueLimit {
// 隊(duì)列大小默認(rèn)為2的30次方.
newSize = dequeueLimit
}
// 賦值鏈表前后節(jié)點(diǎn)關(guān)系.
// prev.
// d2.prev=d1.
// d1.next=d2.
d2 := &poolChainElt{prev: d}
d2.vals = make([]eface, newSize)
c.head = d2
// next .
storePoolChainElt(&d.next, d2)
d2.pushHead(val)
}
// 入隊(duì)poolDequeue
func (d *poolDequeue) pushHead(val any) bool {
ptrs := atomic.LoadUint64(&d.headTail)
head, tail := d.unpack(ptrs)
// head 表示當(dāng)前有多少元素.
if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head {
return false
}
// 環(huán)型隊(duì)列. head&uint32(len(d.vals)-1) 表示當(dāng)前元素落的位置一定在隊(duì)列上.
slot := &d.vals[head&uint32(len(d.vals)-1)]
typ := atomic.LoadPointer(&slot.typ)
if typ != nil {
return false
}
// The head slot is free, so we own it.
if val == nil {
val = dequeueNil(nil)
}
// 向slot寫入指針類型為*any,并且值為val.
*(*any)(unsafe.Pointer(slot)) = val
// headTail高32位++
atomic.AddUint64(&d.headTail, 1<<dequeueBits)
return true
}Get實(shí)現(xiàn)邏輯:
其實(shí)我們看了Put相關(guān)邏輯之后,我們可能很自然的就想到了Get的邏輯,無非就是遍歷鏈表,并且如果隊(duì)列中最后一個(gè)元素不為空,則會將該元素返回,并且將該插槽賦值為空值.
二、學(xué)習(xí)收獲
如何自己實(shí)現(xiàn)一個(gè)無鎖隊(duì)列. 本文未實(shí)現(xiàn),后續(xù)文章會進(jìn)行實(shí)現(xiàn).
2.1 如何自己實(shí)現(xiàn)一個(gè)無鎖隊(duì)列
橫向思考,并未進(jìn)行實(shí)現(xiàn),后續(xù)會進(jìn)行實(shí)現(xiàn)“
- 存儲直接使用指針來進(jìn)行存儲,充分利用
uintptr和unsafe.Pointer和結(jié)構(gòu)體指針之間的依賴關(guān)系來提升性能. - 狀態(tài)存儲要考慮CPU Cache Line、內(nèi)存對齊以及減少訪問內(nèi)存次數(shù)等相關(guān)問題.
- 充分利用Go中的原子操作包來進(jìn)行實(shí)現(xiàn),通過
atomic.CompareAndSwapPointer來設(shè)計(jì)自旋來達(dá)到無鎖化.
到此這篇關(guān)于深入淺出Golang中的sync.Pool的文章就介紹到這了,更多相關(guān)Golang sync.Pool內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang中defer執(zhí)行時(shí)機(jī)的案例分析
這篇文章主要來通過一些案例和大家一起探討一下golang中defer的執(zhí)行時(shí)機(jī),文中的示例代碼講解詳細(xì),對我們深入了解golang有一定的幫助,感興趣的可以跟隨小編一起學(xué)習(xí)一下2023-11-11
Golang中interface{}轉(zhuǎn)為數(shù)組的操作
這篇文章主要介紹了Golang中interface{}轉(zhuǎn)為數(shù)組的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
Go項(xiàng)目與Docker結(jié)合實(shí)現(xiàn)高效部署深入探究
在現(xiàn)代軟件開發(fā)中,使用Docker部署應(yīng)用程序已經(jīng)成為一種標(biāo)準(zhǔn)實(shí)踐,本文將深入探討如何將Go項(xiàng)目與Docker結(jié)合,實(shí)現(xiàn)高效、可靠的部署過程,通過詳細(xì)的步驟和豐富的示例,你將能夠迅速掌握這一流程2023-12-12
解決Golang并發(fā)工具Singleflight的問題
前段時(shí)間在一個(gè)項(xiàng)目里使用到了分布式鎖進(jìn)行共享資源的訪問限制,后來了解到Golang里還能夠使用singleflight對共享資源的訪問做限制,于是利用空余時(shí)間了解,將知識沉淀下來,并做分享2022-05-05
go語言實(shí)現(xiàn)通過FTP庫自動上傳web日志
這篇文章主要介紹了go語言實(shí)現(xiàn)通過FTP庫自動上傳web日志,非常簡單實(shí)用,需要的小伙伴快來參考下吧。2015-03-03
golang實(shí)現(xiàn)對JavaScript代碼混淆
在Go語言中,你可以使用一些工具來混淆JavaScript代碼,一個(gè)常用的工具是Terser,它可以用于壓縮和混淆JavaScript代碼,你可以通過Go語言的`os/exec`包來調(diào)用Terser工具,本文給通過一個(gè)簡單的示例給大家介紹一下,感興趣的朋友可以參考下2024-01-01

