Golang sync.Pool的源碼解析
實(shí)際使用
Pool
是用于存放臨時(shí)對象的集合,這些對象是為了后續(xù)的使用,以達(dá)到復(fù)用對象的效果。其目的是緩解頻繁創(chuàng)建對象造成的gc壓力。在許多開源組件中均使用了此組件,例如bolt
、gin
等。
下面是一組在非并發(fā)和并發(fā)場景是否使用Pool
的benchmark:
package pool import ( "io/ioutil" "sync" "testing" ) type Data [1024]byte // 直接創(chuàng)建對象 func BenchmarkWithoutPool(t *testing.B) { for i := 0; i < t.N; i++ { var data Data ioutil.Discard.Write(data[:]) } } // 使用Pool復(fù)用對象 func BenchmarkWithPool(t *testing.B) { pool := &sync.Pool{ // 若沒有可用對象,則調(diào)用New創(chuàng)建一個(gè)對象 New: func() interface{} { return &Data{} }, } for i := 0; i < t.N; i++ { // 取 data := pool.Get().(*Data) // 用 ioutil.Discard.Write(data[:]) // 存 pool.Put(data) } } // 并發(fā)的直接創(chuàng)建對象 func BenchmarkWithoutPoolConncurrency(t *testing.B) { t.RunParallel(func(pb *testing.PB) { for pb.Next() { var data Data ioutil.Discard.Write(data[:]) } }) } // 使用Pool并發(fā)的復(fù)用對象 func BenchmarkWithPoolConncurrency(t *testing.B) { pool := &sync.Pool{ // 若沒有可用對象,則調(diào)用New創(chuàng)建一個(gè)對象 New: func() interface{} { return &Data{} }, } t.RunParallel(func(pb *testing.PB) { for pb.Next() { // 取 data := pool.Get().(*Data) // 用 ioutil.Discard.Write(data[:]) // 存 pool.Put(data) } }) }
實(shí)際運(yùn)行效果如下圖所示,可以看出sync.Pool
不管是在并發(fā)還是非并發(fā)場景下,在速度和內(nèi)存分配上表現(xiàn)均遠(yuǎn)遠(yuǎn)優(yōu)異于直接創(chuàng)建對象。
goos: darwin goarch: amd64 pkg: leetcode/pool cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz BenchmarkWithoutPool-8 7346660 148.1 ns/op 1024 B/op 1 allocs/op BenchmarkWithPool-8 80391398 14.41 ns/op 0 B/op 0 allocs/op BenchmarkWithoutPoolConncurrency-8 7893248 153.3 ns/op 1024 B/op 1 allocs/op BenchmarkWithPoolConncurrency-8 363329767 4.245 ns/op 0 B/op 0 allocs/op PASS ok leetcode/pool 6.590s
實(shí)現(xiàn)原理
Pool
基本結(jié)構(gòu)如下
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 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() any }
其中最為主要的是屬性 local
,是一個(gè)和P數(shù)量一致的切片,每個(gè)P的id都對應(yīng)切片中的一個(gè)元素。為了高效的利用CPU多核,元素中間填充了pad,具體細(xì)節(jié)可以參考后續(xù)的 CacheLine
。
// Local per-P Pool appendix. type poolLocalInternal struct { private any // 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 . pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte }
CacheLine
CPU
緩存會按照CacheLine大小來從內(nèi)存復(fù)制數(shù)據(jù),相鄰的數(shù)據(jù)可能會處于同一個(gè) CacheLine
。如果這些數(shù)據(jù)被多核使用,那么系統(tǒng)需要耗費(fèi)較大的資源來保持各個(gè)cpu緩存中數(shù)據(jù)的一致性。當(dāng)一個(gè)線程修改某個(gè) CacheLine
中數(shù)據(jù)的時(shí)候,其他讀此 CacheLine
數(shù)據(jù)的線程會被鎖給阻塞。
下面是一組在并發(fā)場景下原子性的操作對象Age
屬性的benchmark:
import ( "sync/atomic" "testing" "unsafe" ) type StudentWithCacheLine struct { Age uint32 _ [128 - unsafe.Sizeof(uint32(0))%128]byte } // 有填充的場景下,并發(fā)修改Age func BenchmarkWithCacheLine(b *testing.B) { count := 10 students := make([]StudentWithCacheLine, count) b.RunParallel(func(pb *testing.PB) { for pb.Next() { for j := 0; j < count; j++ { atomic.AddUint32(&students[j].Age, 1) } } }) } type StudentWithoutCacheLine struct { Age uint32 } // 無填充的場景下,并發(fā)修改Age func BenchmarkWithoutCacheLine(b *testing.B) { count := 10 students := make([]StudentWithoutCacheLine, count) b.RunParallel(func(pb *testing.PB) { for pb.Next() { for j := 0; j < count; j++ { atomic.AddUint32(&students[j].Age, 1) } } }) }
StudentWithCacheLine
中填充了pad來保證切片中不同的Age處于不同的CacheLine, StudentWithoutCacheLine
中的Age未做任何處理。通過圖可以知道根據(jù) CacheLine
填充了pad的Age
原子操作速度遠(yuǎn)遠(yuǎn)快于未做任何處理的。
goos: darwin goarch: amd64 pkg: leetcode/pool cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz BenchmarkWithCacheLine-8 17277380 70.18 ns/op 0 B/op 0 allocs/op BenchmarkWithoutCacheLine-8 8916874 133.8 ns/op 0 B/op 0 allocs/op
生產(chǎn)消費(fèi)者模型
Pool
的高性能不僅僅使用CacheLine
避免了多核之間的數(shù)據(jù)競爭,還根據(jù)GMP
模型使用了生產(chǎn)者消費(fèi)者模型來減少數(shù)據(jù)競爭,每個(gè)P都對應(yīng)一個(gè)poolLocalInternal
。以較為復(fù)雜的Get
的流程,取數(shù)流程如下:
// Get selects an arbitrary item from the Pool, removes it from the // Pool, and returns it to the caller. // Get may choose to ignore the pool and treat it as empty. // Callers should not assume any relation between values passed to Put and // the values returned by Get. // // If Get would otherwise return nil and p.New is non-nil, Get returns // the result of calling p.New. func (p *Pool) Get() any { if race.Enabled { race.Disable() } // 1. 找到當(dāng)前goroutine所在的P對應(yīng)的poolLocalInternal和P對應(yīng)的id l, pid := p.pin() x := l.private l.private = nil // 2. 如果private是空,在從shared進(jìn)行popHead if x == nil { // Try to pop the head of the local shard. We prefer // the head over the tail for temporal locality of // reuse. // 3. 嘗試從shared上取值 x, _ = l.shared.popHead() // 4. 如果還如空,則嘗試從其他P的poolLocalInternal或者victim中獲取 if x == nil { x = p.getSlow(pid) } } runtime_procUnpin() if race.Enabled { race.Enable() if x != nil { race.Acquire(poolRaceAddr(x)) } } // 5. 如果還如空,則直接使用New初始化一個(gè) if x == nil && p.New != nil { x = p.New() } return x }
優(yōu)先在當(dāng)前goroutine所在P對應(yīng)的
poolLocalInternal
上找,先找private
,再找shared
判斷
private
是否有值。對于每個(gè)P來說是單線程的,取private
的時(shí)候是不用鎖,僅僅簡單判斷即可。如果有值直接返回即可;如果為空,再查找shared
。查看
shared
是否有值。shared是一個(gè)雙向鏈表,鏈起來的是ringbuf(環(huán)形數(shù)組),在添加ringbuf的時(shí)候,其大小是前一個(gè)的兩倍。對于goroutine來說,既是當(dāng)前P上取值的消費(fèi)者,又是當(dāng)前P上存值的生產(chǎn)者。在這兩種場景是使用方法分別是:取值使用
**popHead**
;存值使用**pushHead**
。均是從head
取數(shù)據(jù)。
// poolChain is a dynamically-sized version of poolDequeue. // // This is implemented as a doubly-linked list queue of poolDequeues // where each dequeue is double the size of the previous one. Once a // dequeue fills up, this allocates a new one and only ever pushes to // the latest dequeue. Pops happen from the other end of the list and // once a dequeue is exhausted, it gets removed from the list. type poolChain struct { // head is the poolDequeue to push to. This is only accessed // by the producer, so doesn't need to be synchronized. head *poolChainElt // tail is the poolDequeue to popTail from. This is accessed // by consumers, so reads and writes must be atomic. tail *poolChainElt } type poolChainElt struct { poolDequeue // next and prev link to the adjacent poolChainElts in this // poolChain. // // next is written atomically by the producer and read // atomically by the consumer. It only transitions from nil to // non-nil. // // prev is written atomically by the consumer and read // atomically by the producer. It only transitions from // non-nil to nil. next, prev *poolChainElt }
如果
private
和shared
均沒值,就嘗試從其他 P 的poolLocalInternal
上取值。這個(gè)時(shí)候就是goroutine扮演的就是消費(fèi)者的角色了,使用的方式是**
popTail
。**從 tail 取數(shù)據(jù)。如果其他
poolLocalInternal
上也沒有值的話,就需要從victim
中取值了。這個(gè)victim
就是跨越GC
遺留下的數(shù)據(jù)。如果都沒有的話,就只能使用
New
創(chuàng)建一個(gè)新的值了。
此模型減少了數(shù)據(jù)的競爭,保證了CAS的高效率。對于處于一個(gè)P上的多個(gè)goroutine來說是單線程的,數(shù)據(jù)之間不會有競爭關(guān)系。每個(gè)goroutine取值的時(shí)候,優(yōu)先從對應(yīng)P上的鏈表頭部取值。只有在鏈表無數(shù)據(jù)的時(shí)候,才會嘗試從其他P上的對應(yīng)的鏈表尾部取值。也就是說出現(xiàn)競爭的可能性的地方在于,一個(gè)goruotine從鏈表頭部取值或者塞值,另外一個(gè)goroutine從鏈表尾部取值,兩者出現(xiàn)沖突的可能性較小。
結(jié)論
總的來說Pool在熱點(diǎn)數(shù)據(jù)競爭上做了很多優(yōu)化,比如CacheLine、GMP、Ringbuf,CAS;另外還跨越GC周期的緩存數(shù)據(jù)。
本文主要就CacheLine和生產(chǎn)者消費(fèi)者模式做了介紹,其他部分感興趣的話可以自行查看源碼。
以上就是Golang sync.Pool的源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Go sync.Pool源碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go語言中GOPATH GOROOT的作用和設(shè)置方式
這篇文章主要介紹了go語言中GOPATH GOROOT的作用和設(shè)置方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05golang原生http包實(shí)現(xiàn)各種情況的get請求方式
這篇文章主要介紹了golang原生http包實(shí)現(xiàn)各種情況的get請求方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08Golang使用Gin實(shí)現(xiàn)文件上傳的示例代碼
本文我們主要介紹了Golang如何使用Gin實(shí)現(xiàn)文件上傳,Go標(biāo)準(zhǔn)庫net/http對文件上傳已經(jīng)提供了非常完善的支持,而Gin框架在其基礎(chǔ)上進(jìn)一步封裝,因此使用Gin開發(fā)文件上傳功能時(shí),只需要簡單幾行代碼便可以實(shí)現(xiàn),需要的朋友可以參考下2024-02-02Golang中類型轉(zhuǎn)換利器cast庫的用法詳解
cast庫是一個(gè)簡潔而強(qiáng)大的第三方庫,它的主要功能是實(shí)現(xiàn)類型之間的安全轉(zhuǎn)換,而在Golang開發(fā)中,類型轉(zhuǎn)換是一個(gè)常見且不可避免的過程,下面我們就來看看cast庫在Golang中的具體應(yīng)用吧2024-11-11