深入淺出Golang中的sync.Pool
學(xué)習(xí)到的內(nèi)容:
1.一個64位的int類型值,充分利用高32位和低32位,進(jìn)行相關(guān)加減以及從一個64位中拆出高32位和低32位.
擴(kuò)展:如何自己實現(xiàn)一個無鎖隊列.
- 如何判斷隊列是否滿.
- 如何實現(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中,每一個P(協(xié)程調(diào)度器)會有一個數(shù)組,數(shù)組大小位localSize. local unsafe.Pointer // p 數(shù)組大小. localSize uintptr New func() any } // poolLocal 每個P(協(xié)程調(diào)度器)的本地pool. type poolLocal struct { poolLocalInternal // 保證一個poolLocal占用一個緩存行 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 過程分析比較重要,因為這里會包含pool所有依賴相關(guān)分析.
總的分析學(xué)習(xí)過程可以分為下面幾個步驟:
1.獲取P
對應(yīng)的poolLocal
2.val
如何進(jìn)入poolLocal
下面的poolDequeue
隊列中的.
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
用一個值來進(jìn)行表示,減少CPU訪問內(nèi)存次數(shù).
獲取P對應(yīng)的poolLocal
sync.Pool.local
其實是一個指針,并且通過變量+結(jié)構(gòu)體大小來劃分內(nèi)存空間,從而將這片內(nèi)存直接劃分為數(shù)組. Go 在Put
之前會先對當(dāng)前Goroutine綁定到當(dāng)前P中,然后通過pid
獲取其在local
內(nèi)存地址中的歧視指針,在獲取時是會進(jìn)行內(nèi)存分配的. 具體如下:
func (p *Pool) pin() (*poolLocal, int) { // 返回運行當(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真實的指針. // unsafe.Pointer Go對指針的封裝: 用于指針和結(jié)構(gòu)體互相轉(zhuǎn)化. return (*poolLocal)(lp) }
從上面代碼我們可以看到,Go通過runtime_procPin
來設(shè)置當(dāng)前Goroutine獨占P,并且直接通過頭指針+偏移量(數(shù)組結(jié)構(gòu)體大小)來進(jìn)行對內(nèi)存劃分為數(shù)組.
Put 進(jìn)入poolDequeue隊列:
Go在Push時,會通過headtail
來獲取當(dāng)前隊列內(nèi)元素個數(shù),如果滿了,則會重新構(gòu)建一個環(huán)型隊列poolChainElt
,并且設(shè)置為poolChain.head
,并且賦值next
以及prev
.
通過下面代碼,我們可以看到,Go通過邏輯運算判斷隊列是否滿的設(shè)計時非常巧妙的,如果后續(xù)我們?nèi)ラ_發(fā)組件,也是可以這么進(jìn)行設(shè)計的。
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) } // 入隊. if d.pushHead(val) { return } // 隊列滿了. newSize := len(d.vals) * 2 if newSize >= dequeueLimit { // 隊列大小默認(rèn)為2的30次方. newSize = dequeueLimit } // 賦值鏈表前后節(jié)點關(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) } // 入隊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)型隊列. head&uint32(len(d.vals)-1) 表示當(dāng)前元素落的位置一定在隊列上. 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實現(xiàn)邏輯:
其實我們看了Put
相關(guān)邏輯之后,我們可能很自然的就想到了Get
的邏輯,無非就是遍歷鏈表,并且如果隊列中最后一個元素不為空,則會將該元素返回,并且將該插槽賦值為空值.
二、學(xué)習(xí)收獲
如何自己實現(xiàn)一個無鎖隊列. 本文未實現(xiàn),后續(xù)文章會進(jìn)行實現(xiàn).
2.1 如何自己實現(xiàn)一個無鎖隊列
橫向思考,并未進(jìn)行實現(xiàn),后續(xù)會進(jìn)行實現(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)行實現(xiàn),通過
atomic.CompareAndSwapPointer
來設(shè)計自旋來達(dá)到無鎖化.
到此這篇關(guān)于深入淺出Golang中的sync.Pool的文章就介紹到這了,更多相關(guān)Golang sync.Pool內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言中利用http發(fā)起Get和Post請求的方法示例
這篇文章主要給大家介紹了關(guān)于Go語言中利用http發(fā)起Get和Post請求的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11詳解golang執(zhí)行Linux shell命令完整場景下的使用方法
本文主要介紹了golang執(zhí)行Linux shell命令完整場景下的使用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06Go語言基礎(chǔ)函數(shù)包的使用學(xué)習(xí)
本文通過一個實現(xiàn)加減乘除運算的小程序來介紹go函數(shù)的使用,以及使用函數(shù)的注意事項,并引出了對包的了解和使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05go內(nèi)存緩存BigCache實現(xiàn)BytesQueue源碼解讀
這篇文章主要為大家介紹了go內(nèi)存緩存BigCache實現(xiàn)BytesQueue源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09