golang中神奇的sync.Pool

前言

在 golang 中有一個(gè)池淤井,它特別神奇,你只要和它有個(gè)約定工猜,你要什么它就給什么米诉,你用完了還可以還回去,但是下次拿的時(shí)候呢篷帅,確不一定是你上次存的那個(gè)史侣,這個(gè)池就是 sync.Pool

說(shuō)實(shí)話第一次看到這個(gè)東西的時(shí)候,真的想不到這個(gè)東西有啥用啊魏身,為什么要有這個(gè)東西呢惊橱?等我看完之后,嗯叠骑,還有有點(diǎn)用的李皇;等到有一次優(yōu)化經(jīng)歷的時(shí)候削茁,嗯宙枷,這個(gè)有點(diǎn)意思了掉房。今天我們就來(lái)看看這個(gè)神奇的 sync.Pool

簡(jiǎn)單案例

首先我們來(lái)看看這個(gè) sync.Pool 是如何使用的,其實(shí)非常的簡(jiǎn)單慰丛。
它一共只有三個(gè)方法我們需要知道的:New卓囚、Put、Get


package main

import (
    "fmt"
    "sync"
)

var strPool = sync.Pool{
    New: func() interface{} {
        return "test str"
    },
}

func main() {
    str := strPool.Get()
    fmt.Println(str)
    strPool.Put(str)
}

  • 通過(guò)New去定義你這個(gè)池子里面放的究竟是什么東西诅病,在這個(gè)池子里面你只能放一種類型的東西哪亿。比如在上面的例子中我就在池子里面放了字符串。
  • 我們隨時(shí)可以通過(guò)Get方法從池子里面獲取我們之前在New里面定義類型的數(shù)據(jù)贤笆。
  • 當(dāng)我們用完了之后可以通過(guò)Put方法放回去蝇棉,或者放別的同類型的數(shù)據(jù)進(jìn)去。

目的

那么這個(gè)池子的目的是什么呢芥永?其實(shí)一句話就可以說(shuō)明白篡殷,就是為了復(fù)用已經(jīng)使用過(guò)的對(duì)象,來(lái)達(dá)到優(yōu)化內(nèi)存使用和回收的目的埋涧。說(shuō)白了板辽,一開(kāi)始這個(gè)池子會(huì)初始化一些對(duì)象供你使用,如果不夠了呢棘催,自己會(huì)通過(guò)new產(chǎn)生一些劲弦,當(dāng)你放回去了之后這些對(duì)象會(huì)被別人進(jìn)行復(fù)用,當(dāng)對(duì)象特別大并且使用非常頻繁的時(shí)候可以大大的減少對(duì)象的創(chuàng)建和回收的時(shí)間醇坝。

來(lái)看看doc

其實(shí)官方文檔里面給出了一些小細(xì)節(jié)讓我們一起來(lái)看看

A Pool is a set of temporary objects that may be individually saved and retrieved.

Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.

A Pool is safe for use by multiple goroutines simultaneously.

Pool's purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists.

An appropriate use of a Pool is to manage a group of temporary items silently shared among and potentially reused by concurrent independent clients of a package. Pool provides a way to amortize allocation overhead across many clients.

An example of good use of a Pool is in the fmt package, which maintains a dynamically-sized store of temporary output buffers. The store scales under load (when many goroutines are actively printing) and shrinks when quiescent.

On the other hand, a free list maintained as part of a short-lived object is not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.

A Pool must not be copied after first use.

注意其中加粗的部分邑跪,我列一下其中的點(diǎn),建議還是嘗試去閱讀doc里面的說(shuō)明呼猪。

  • 臨時(shí)對(duì)象
  • 自動(dòng)移除
  • 當(dāng)這個(gè)對(duì)象的引用只有sync.Pool持有時(shí)呀袱,這個(gè)對(duì)象內(nèi)存會(huì)被釋放
  • 多線程安全
  • 目的就是緩存并重用對(duì)象,減少GC的壓力
  • 自動(dòng)擴(kuò)容郑叠、縮容
  • 不要去拷貝pool夜赵,也就是說(shuō)最好單例

源碼分析

下面我們從源碼層面來(lái)看看這個(gè) sync.Pool;可能需要你有GPM模型和GC的相關(guān)知識(shí)乡革。
使用golang版本: go version go1.13

結(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() interface{}
}

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{} // 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
}

我們可以看到其實(shí)結(jié)構(gòu)并不復(fù)雜寇僧,但是如果自己看的話有點(diǎn)懵。注意幾個(gè)細(xì)節(jié)就ok沸版。

  • local這里面真正的是[P]poolLocal其中P就是GPM模型中的P嘁傀,有多少個(gè)P數(shù)組就有多大,也就是每個(gè)P維護(hù)了一個(gè)本地的poolLocal视粮。
  • poolLocal里面維護(hù)了一個(gè)private一個(gè)shared细办,看名字其實(shí)就很明顯了,private是給自己用的,而shared的是一個(gè)隊(duì)列笑撞,可以給別人用的岛啸。注釋寫(xiě)的也很清楚,自己可以從隊(duì)列的頭部存然后從頭部取茴肥,而別的P可以從尾部取坚踩。
  • victim這個(gè)從字面上面也可以知道,幸存者嘛瓤狐,當(dāng)進(jìn)行g(shù)c的stw時(shí)候瞬铸,會(huì)將local中的對(duì)象移到victim中去,也就是說(shuō)幸存了一次gc础锐,

Get

func (p *Pool) Get() interface{} {
    ......
    l, pid := p.pin()
    x := l.private
    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()
        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{} {
    // 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.
    for i := 0; i < int(size); i++ {
        l := indexLocal(locals, (pid+i+1)%int(size))
        if x, _ := l.shared.popTail(); x != nil {
            return x
        }
    }

    // Try the victim cache. We do this after attempting to steal
    // from all primary caches because we want objects in the
    // victim cache to age out if at all possible.
    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
        }
    }

    // Mark the victim cache as empty for future gets don't bother
    // with it.
    atomic.StoreUintptr(&p.victimSize, 0)

    return nil
}

我去掉了其中一些競(jìng)態(tài)分析的代碼嗓节,Get的邏輯其實(shí)非常清晰。

  • 如果 private 不是空的皆警,那就直接拿來(lái)用
  • 如果 private 是空的赦政,那就先去本地的shared隊(duì)列里面從頭 pop 一個(gè)
  • 如果本地的 shared 也沒(méi)有了,那 getSlow 去拿耀怜,其實(shí)就是去別的P的 shared 里面偷恢着,偷不到回去 victim 幸存者里面找
  • 如果最后都沒(méi)有,那就只能調(diào)用 New 方法創(chuàng)建一個(gè)了

我隨手畫(huà)了一下财破,可能不是特別準(zhǔn)確掰派,意思到位了

-w566

Put

// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
    if x == nil {
        return
    }
    ......
    l, _ := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    if x != nil {
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
    ......
}

看完Get其實(shí)Put就很簡(jiǎn)單了

  • 如果 private 沒(méi)有,就放在 private
  • 如果 private 有了左痢,那么就放到 shared 隊(duì)列的頭部

實(shí)際測(cè)試

讓我們實(shí)際寫(xiě)個(gè)測(cè)試的案例來(lái)測(cè)測(cè)具體使用時(shí)會(huì)有什么樣的變化

Put之后馬上Get

var pool = sync.Pool{
    New: func() interface{} {
        return "123"
    },
}

func main() {
    t := pool.Get().(string)
    fmt.Println(t)

    pool.Put("321")
    
    t2 := pool.Get().(string)
    fmt.Println(t2)
}

輸出:
123
321

Put之后GC后Get

var pool = sync.Pool{
    New: func() interface{} {
        return "123"
    },
}

func main() {
    t := pool.Get().(string)
    fmt.Println(t)

    pool.Put("321")
    pool.Put("321")
    pool.Put("321")
    pool.Put("321")

    runtime.GC()
    time.Sleep(1 * time.Second)

    t2 := pool.Get().(string)
    fmt.Println(t2)

    runtime.GC()
    time.Sleep(1 * time.Second)

    t2 = pool.Get().(string)
    fmt.Println(t2)
}

輸出:
123
321
123

你知道為什么嗎靡羡?

總結(jié)

這次總結(jié)來(lái)點(diǎn)不一樣的,提幾個(gè)問(wèn)題吧俊性。

  1. 什么情況下適合使用sync.Pool呢略步?
  2. sync.Pool的對(duì)象什么時(shí)候會(huì)被回收呢?
  3. sync.Pool是如何實(shí)現(xiàn)線程安全的定页?
    如果你能回答上面的問(wèn)題趟薄,證明你對(duì)它已經(jīng)足夠了解了,那么就可以嘗試在具體的情況下使用它來(lái)玩玩了典徊。試試吧~
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杭煎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子卒落,更是在濱河造成了極大的恐慌羡铲,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儡毕,死亡現(xiàn)場(chǎng)離奇詭異也切,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)雷恃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)疆股,“玉大人,你說(shuō)我怎么就攤上這事褂萧。” “怎么了葵萎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵导犹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我羡忘,道長(zhǎng)谎痢,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任卷雕,我火速辦了婚禮节猿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘漫雕。我一直安慰自己滨嘱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布浸间。 她就那樣靜靜地躺著太雨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪魁蒜。 梳的紋絲不亂的頭發(fā)上囊扳,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音兜看,去河邊找鬼锥咸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛细移,可吹牛的內(nèi)容都是我干的搏予。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼弧轧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缔刹!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起劣针,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤校镐,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后捺典,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鸟廓,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了引谜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牍陌。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖员咽,靈堂內(nèi)的尸體忽然破棺而出毒涧,到底是詐尸還是另有隱情,我是刑警寧澤贝室,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布契讲,位于F島的核電站,受9級(jí)特大地震影響滑频,放射性物質(zhì)發(fā)生泄漏捡偏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一峡迷、第九天 我趴在偏房一處隱蔽的房頂上張望银伟。 院中可真熱鬧,春花似錦绘搞、人聲如沸彤避。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忠藤。三九已至,卻和暖如春楼雹,著一層夾襖步出監(jiān)牢的瞬間模孩,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工贮缅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榨咐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓谴供,卻偏偏與公主長(zhǎng)得像块茁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桂肌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

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