【Go夜讀】sync.Pool 源碼閱讀及適用場(chǎng)景分析

資源鏈接


B站視頻

介紹


  • 就是一個(gè)池子慰照,可以暫時(shí)存儲(chǔ)對(duì)象并查詢對(duì)象食绿。
  • 任何存儲(chǔ)在池子里面的對(duì)象可能會(huì)被自動(dòng)移除(GC時(shí)),如果此時(shí)池子僅僅保留了引用,那么對(duì)象將會(huì)被 deallocated蠢甲。
  • 一個(gè)池子可以被多個(gè)goroutines同時(shí)安全地訪問遂鹊。

引發(fā)的關(guān)于 Cache 和 Pool 的爭(zhēng)論

golang sync.Pool試用說明及注意事項(xiàng)

gc(garbage collector)

  • Go 是自動(dòng)垃圾回收的肝集,減少了程序員的負(fù)擔(dān)
  • GC 是一把雙刃劍抓谴,帶來便利但是也增加了開銷,使用不當(dāng)會(huì)嚴(yán)重影響程序的性能
  • 高性能場(chǎng)景下怨愤,不能任意產(chǎn)生太多的垃圾(GC 負(fù)擔(dān)重派敷,會(huì)影響性能)

如何解決GC負(fù)擔(dān)重?

  • 避免大家重復(fù)造輪子撰洗,開發(fā)了 Pool 包來保存和復(fù)用臨時(shí)對(duì)象篮愉,以減少內(nèi)存分配,降低 GC 壓力
  • http://echo.labstack.com/guide/routing
    Echo 的路由使用了 sync pool 來重復(fù)利用內(nèi)存并且?guī)缀踹_(dá)到了零內(nèi)存占用
  • gin 的 context 通過 pool 來 get 和 put差导,也就是使用了 sync.Pool 進(jìn)行維護(hù)

兩種使用方式

// 方法一
package main
import(
    "fmt"
    "log"
    "runtime"
    "sync"
)

func main(){
    p := &sync.Pool{
        New: func() interface{} {
            return 0
        },
    }

    a := p.Get().(int)
    p.Put(1)
    b := p.Get().(int)
    fmt.Println(a, b)  // 輸出 0 1
    p.Put(3)
    p.Put(4)
    p.Put(5)
    log.Println(p.Get()) // 返回 3 4 5 中的任意一個(gè)
    // 主動(dòng)調(diào)用 GC试躏, pool 中的對(duì)象會(huì)被列入 victim 緩存
    runtime.GC()
    c := p.Get().(int)
    log.Println(c)  // 拿到的是 4
    // 再次調(diào)用 GC, pool 中的 victim 緩存會(huì)被刪除
    runtime.GC()
    c = p.Get().(int)
    log.Println(c)  // 拿到的是 0
}
// 方法二
package main
import(
    "fmt"
    "sync"
)

func main(){
    // 如果我們不指定 New 函數(shù)的話设褐,會(huì)返回 nil
    p := &sync.Pool{}
    a := p.Get()
    if a == nil {
        a = func() interface{} {
            return 0
        }
    }
    
    p.Put(1)
    b := p.Get().(int)
    fmt.Println(a, b)  // 輸出 0 1
}

Pool 源碼解讀


Pool 結(jié)構(gòu)

type Pool struct {
    // 用來標(biāo)記颠蕴,當(dāng)前的 struct 是不能夠被 copy 的
    noCopy noCopy
    // P 個(gè)固定大小的 poolLocal 數(shù)組,每個(gè) P 擁有一個(gè)空間
    local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
    // 上面數(shù)組的大小络断,即 P 的個(gè)數(shù)
    localSize uintptr        // size of the local array

    // 同 local 和 localSize裁替,只是在 gc 的過程中保留一次
    victim     unsafe.Pointer // local from previous cycle
    victimSize uintptr        // size of victims array

    // 自定義一個(gè) New 函數(shù),然后可以在 Get 不到東西時(shí)貌笨,自動(dòng)創(chuàng)建一個(gè)
    New func() interface{}
}

因?yàn)?Pool 不希望被復(fù)制,所以結(jié)構(gòu)體里有一個(gè) noCopy 的字段襟沮,使用 go vet 工具可以檢測(cè)到用戶代碼是否復(fù)制了 Pool锥惋。noCopy 是 go1.7 開始引入的一個(gè)靜態(tài)檢查機(jī)制昌腰。它不僅僅工作在運(yùn)行時(shí)或標(biāo)準(zhǔn)庫,同時(shí)也對(duì)用戶代碼有效膀跌。用戶只需實(shí)現(xiàn)這樣的不消耗內(nèi)存遭商、僅用于靜態(tài)分析的結(jié)構(gòu),來保證一個(gè)對(duì)象在第一次使用后不會(huì)發(fā)生復(fù)制捅伤。

// Local per-P Pool appendix.
type poolLocalInternal struct {
    // private 存儲(chǔ)一個(gè) Put 的數(shù)據(jù)劫流,pool.Put() 操作優(yōu)先存入 private,如果private有信息丛忆,才會(huì)存入 shared
    private interface{} // Can be used only by the respective P.
    // 存儲(chǔ)一個(gè)鏈表祠汇,用來維護(hù) pool.Put() 操作加入的數(shù)據(jù),每個(gè) P 可以操作自己 shared 鏈表中的頭部熄诡,而其他的 P 在用完自己的 shared 時(shí)可很,可能會(huì)來偷數(shù)據(jù),從而操作鏈表的尾部
    shared  poolChain   // Local P can pushHead/popHead; any P can popTail.
}
// unsafe.Sizeof(poolLocal{})  // 128 byte(1byte = 8 bits)
// unsafe.Sizeof(poolLocalInternal{})  // 32 byte(1byte = 8 bits)
type poolLocal struct {
    poolLocalInternal

    // Prevents false sharing on widespread platforms with
    // 128 mod (cache line size) = 0 .
    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

Get()

func (p *Pool) Get() interface{} {
    ...
    l, pid := p.pin()  // 獲取當(dāng)前 pool 的 poolLocal凰浮,也就是 p.local[pid]
    x := l.private  // 判斷當(dāng)前的臨時(shí)變量是否有值我抠,有則立即返回
    l.private = nil
    if x == nil {
        // Try to pop the head of the local shard. We prefer
        // the head over the tail for temporal locality of
        // reuse.
        x, _ = l.shared.popHead()  // 從 shared poolChain 鏈表里面獲取頭部數(shù)據(jù)
        if x == nil {
            x = p.getSlow(pid)  // 本線程的 Pool 沒有數(shù)據(jù)了,就去其他線程的 Pool 池取
        }
    }
    ...
    // 無法獲取到值袜茧,則 New 一個(gè)菜拓,未設(shè)定 New 函數(shù)則返回 nil
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

Put()

func (p *Pool) Put(x interface{}) {
    if x == nil {
        return
    }
    ...
    l, _ := p.pin()  // 獲取當(dāng)前 pool 的 poolLocal,也就是 p.local[pid]笛厦,這里不關(guān)心 pid
    // 優(yōu)先寫入 private 變量
    if l.private == nil {
        l.private = x
        x = nil
    }
    // 如果 private 有值纳鼎,則寫入 shared poolChain 鏈表
    if x != nil {
        l.shared.pushHead(x)
    }
    ...
}

indexLocal()

獲取線程 pid (i) 對(duì)應(yīng)的 poolLocal,因?yàn)槭莻€(gè)數(shù)組递递,即 0+offset

func indexLocal(l unsafe.Pointer, i int) *poolLocal {
    lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
    return (*poolLocal)(lp)
}

getSlow()

func (p *Pool) getSlow(pid int) interface{} {
    // See the comment in pin regarding ordering of the loads.
    size := atomic.LoadUintptr(&p.localSize) // load-acquire
    locals := p.local                        // load-consume
    // Try to steal one element from other procs. 從其他的線程偷數(shù)據(jù)
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i+1)%int(size))  // 從當(dāng)前 pid 的 local 開始遍歷其他線程的 pool 池(poolLocal)喷橙,遍歷一個(gè)圈。返回值為其他線程的 pool.poolLocal 
        // 從尾部獲取數(shù)據(jù)
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // 當(dāng)無法從其他線程的 poolLocal 得到信息登舞,則從 victim 緩存區(qū)域獲确∮狻(和 local 一樣的邏輯)
    size = atomic.LoadUintptr(&p.victimSize)
    if uintptr(pid) >= size {
        return nil
    }
    locals = p.victim
    l := indexLocal(locals, pid)
    if x := l.private; x != nil {
        l.private = nil
        return x
    }
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // 如果 victim 全空,則 victimSize 設(shè)置為 0菠秒,防止下次再次遍歷
    atomic.StoreUintptr(&p.victimSize, 0)

    return nil
}

pin()

pin 函數(shù) pins 當(dāng)前 goroutine 的 P疙剑,防止 preemption
returns poolLocal pool for the P and the P's id.
調(diào)用方當(dāng)完成對(duì) pool 的操作后,必須調(diào)用 runtime_procUnpin()

func (p *Pool) pin() (*poolLocal, int) {
    pid := runtime_procPin()
    // In pinSlow we store to local and then to localSize, here we load in opposite order.
    // Since we've disabled preemption, GC cannot happen in between.
    // Thus here we must observe local at least as large localSize.
    // We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
    s := atomic.LoadUintptr(&p.localSize) // load-acquire
    l := p.local                          // load-consume
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid  // 獲取當(dāng)前線程的 poolLocal 和 pid
    }
    return p.pinSlow()
}

// 該函數(shù)鏈接于 runtime.proc.go:sync_runtime_procPin 函數(shù)
func runtime_procPin(){}

//go:linkname sync_runtime_procPin sync.runtime_procPin
//go:nosplit
func sync_runtime_procPin() int {
    return procPin()
}

//go:nosplit
func procPin() int {
    _g_ := getg()
    mp := _g_.m

    mp.locks++
    return int(mp.p.ptr().id)
}

//go:nosplit
func procUnpin() {
    _g_ := getg()
    _g_.m.locks--
}

pinSlow()

func (p *Pool) pinSlow() (*poolLocal, int) {
    // 由于調(diào)用該函數(shù)前 pin 過践叠,這里需要 unpin言缤,否則 allPoolsMu 無法被加鎖
    runtime_procUnpin()
    // 對(duì) allPools 變量加鎖,來操作 allPools禁灼,這里存儲(chǔ)所有的 pool
    allPoolsMu.Lock()
    defer allPoolsMu.Unlock()
    // 重新 pin 當(dāng)前線程的 P
    pid := runtime_procPin()
    // pin 后 poolCleanup 不會(huì)被調(diào)用
    s := p.localSize
    l := p.local
    if uintptr(pid) < s {
        return indexLocal(l, pid), pid
    }
    if p.local == nil {
        allPools = append(allPools, p)
    }
    // 如果 GOMAXPROCS 在 GCs 時(shí)發(fā)生了改變管挟,我們重新分配 local,并設(shè)置 localSize
    size := runtime.GOMAXPROCS(0)  // 獲取線程數(shù)
    local := make([]poolLocal, size)  // 每個(gè)線程一個(gè) poolLocal弄捕,所以這里設(shè)置為 size 個(gè)大小的數(shù)組
    atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
    atomic.StoreUintptr(&p.localSize, uintptr(size))         // store-release
    return &local[pid], pid
}

poolCleanUp()

該函數(shù)在 init 函數(shù)中注冊(cè)到 runtime 中僻孝,在調(diào)用 GC 前导帝,函數(shù)被調(diào)用

func poolCleanup() {
    // Drop victim caches from all pools.
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }

    // Move primary cache to victim cache.
    for _, p := range allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }

    // 所有的池都丟掉主緩存,并數(shù)據(jù)移動(dòng)到 victim 緩存
    oldPools, allPools = allPools, nil
}

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

// 該函數(shù)鏈接于 runtime.mgc.go:sync_runtime_registerPoolCleanup
func runtime_registerPoolCleanup(cleanup func())

//go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup
func sync_runtime_registerPoolCleanup(f func()) {
    poolcleanup = f
}

總結(jié)


sync.Pool 的特性

  1. 池不能夠指定大小穿铆,大小只受限于 GC 的臨界值(GOMAXPROCS)
  2. 對(duì)象最大的緩存周期是兩個(gè) GC 周期您单,每次 GC ,當(dāng)前的 primary cache 會(huì)被轉(zhuǎn)移到 victim cache荞雏,primary cache 清空虐秦,而原來 victim cache 被釋放
  3. 取值順序:當(dāng)前 P 的 primary cache(local)的 poolLocal.private → 當(dāng)前 P 的 primary cache(local)的 poolLocal.shared.head → 其他 P 的主存(local)的 poolLocal.shared.tail → 當(dāng)前 P 的 victim cache(victim) 的 poolLocal.private → 當(dāng)前 P 的 victim cache(victim) 的 poolLocal.shared.head → 其他 P 的主存(local)的 poolLocal.shared.tail → p.New() → nil
  4. 插入順序:當(dāng)前 P 的 primary cache(local)的 poolLocal.private → 當(dāng)前 P 的 primary cache(local)的 poolLocal.shared.head

Use sync.Pool

以下是視頻

工具

Golang 性能剖析工具-pprof

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市凤优,隨后出現(xiàn)的幾起案子悦陋,更是在濱河造成了極大的恐慌,老刑警劉巖别洪,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叨恨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挖垛,警方通過查閱死者的電腦和手機(jī)痒钝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痢毒,“玉大人送矩,你說我怎么就攤上這事∧奶妫” “怎么了栋荸?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)凭舶。 經(jīng)常有香客問我晌块,道長(zhǎng),這世上最難降的妖魔是什么帅霜? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任匆背,我火速辦了婚禮,結(jié)果婚禮上身冀,老公的妹妹穿的比我還像新娘钝尸。我一直安慰自己,他們只是感情好搂根,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布珍促。 她就那樣靜靜地躺著,像睡著了一般剩愧。 火紅的嫁衣襯著肌膚如雪猪叙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音沐悦,去河邊找鬼成洗。 笑死五督,一個(gè)胖子當(dāng)著我的面吹牛藏否,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播充包,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼副签,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了基矮?” 一聲冷哼從身側(cè)響起淆储,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎家浇,沒想到半個(gè)月后本砰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钢悲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年点额,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莺琳。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡还棱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惭等,到底是詐尸還是另有隱情珍手,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布辞做,位于F島的核電站琳要,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏秤茅。R本人自食惡果不足惜稚补,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嫂伞。 院中可真熱鬧孔厉,春花似錦、人聲如沸帖努。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拼余。三九已至污桦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匙监,已是汗流浹背凡橱。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工小作, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稼钩。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓顾稀,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親坝撑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子静秆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 在 golang 中有一個(gè)池,它特別神奇巡李,你只要和它有個(gè)約定抚笔,你要什么它就給什么,你用完了還可以還回去侨拦,但是...
    LinkinStar閱讀 19,658評(píng)論 3 8
  • 官方對(duì)對(duì)象池的定義 // A Pool is a set of temporary objects that ma...
    GGBond_8488閱讀 261評(píng)論 0 1
  • 源碼目錄 ///sync.pool.go (1.14.1) 前言 sync.pool對(duì)象池是個(gè)好東西殊橙,避免對(duì)象的反...
    ihornet閱讀 1,205評(píng)論 0 3
  • golang sync.pool對(duì)象復(fù)用 并發(fā)原理 緩存池 在go http每一次go serve(l)都會(huì)構(gòu)建R...
    fjxCode閱讀 3,969評(píng)論 2 7
  • 今天是個(gè)大好日子呀!因?yàn)槲覀兘裉扉_業(yè)了狱从,我非常開心膨蛮。在昨天晚上,媽媽和爸爸把油煉了矫夯,菜該切的切了鸽疾,弄到很晚...
    梔子花_5599閱讀 230評(píng)論 0 1