Golang sync.Pool 和 偽共享false share

參考
go語言的官方包sync.Pool的實(shí)現(xiàn)原理和適用場(chǎng)景
深入Golang之sync.Pool詳解
偽共享(false sharing)袜茧,并發(fā)編程無聲的性能殺手

一涤垫、簡(jiǎn)述

眾所周知惠啄,go是自動(dòng)垃圾回收的(garbage collector)场仲,這大大減少了程序編程負(fù)擔(dān)橄唬。但gc是一把雙刃劍法竞,帶來了編程的方便但同時(shí)也增加了運(yùn)行時(shí)開銷耙厚,使用不當(dāng)甚至?xí)?yán)重影響程序的性能。因此性能要求高的場(chǎng)景不能任意產(chǎn)生太多的垃圾(有g(shù)c但又不能完全依賴它挺惡心的)岔霸,如何解決呢薛躬?那就是要重用對(duì)象了,我們可以簡(jiǎn)單的使用一個(gè)chan把這些可重用的對(duì)象緩存起來呆细,但如果很多goroutine競(jìng)爭(zhēng)一個(gè)chan性能肯定是問題.....由于golang團(tuán)隊(duì)認(rèn)識(shí)到這個(gè)問題普遍存在型宝,為了避免大家重造車輪,因此官方統(tǒng)一出了一個(gè)包Pool絮爷。但為什么放到sync包里面也是有的迷惑的趴酣,先不討論這個(gè)問題。

先來看看如何使用一個(gè)pool:

package main
 
import(
    "fmt"
    "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)
}

上面創(chuàng)建了一個(gè)緩存int對(duì)象的一個(gè)pool坑夯,先從池獲取一個(gè)對(duì)象然后放進(jìn)去一個(gè)對(duì)象再取出一個(gè)對(duì)象价卤,程序的輸出是0 1。創(chuàng)建的時(shí)候可以指定一個(gè)New函數(shù)渊涝,獲取對(duì)象的時(shí)候如何在池里面找不到緩存的對(duì)象將會(huì)使用指定的new函數(shù)創(chuàng)建一個(gè)返回慎璧,如果沒有new函數(shù)則返回nil。用法是不是很簡(jiǎn)單跨释,我們這里就不多說胸私,下面來說說我們關(guān)心的問題:

1、緩存對(duì)象的數(shù)量和期限

上面我們可以看到pool創(chuàng)建的時(shí)候是不能指定大小的鳖谈,所有sync.Pool的緩存對(duì)象數(shù)量是沒有限制的(只受限于內(nèi)存)岁疼,因此使用sync.pool是沒辦法做到控制緩存對(duì)象數(shù)量的個(gè)數(shù)的。另外sync.pool緩存對(duì)象的期限是很詭異的缆娃,先看一下src/pkg/sync/pool.go里面的一段實(shí)現(xiàn)代碼:

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

可以看到pool包在init的時(shí)候注冊(cè)了一個(gè)poolCleanup函數(shù)捷绒,它會(huì)清除所有的pool里面的所有緩存的對(duì)象,該函數(shù)注冊(cè)進(jìn)去之后會(huì)在每次gc之前都會(huì)調(diào)用贯要,因此sync.Pool緩存的期限只是兩次gc之間這段時(shí)間暖侨。例如我們把上面的例子改成下面這樣之后,輸出的結(jié)果將是0 0崇渗。正因gc的時(shí)候會(huì)清掉緩存對(duì)象字逗,也不用擔(dān)心pool會(huì)無限增大的問題京郑。

    a := p.Get().(int)
    p.Put(1)
    runtime.GC()
    b := p.Get().(int)
    fmt.Println(a, b)

這是很多人錯(cuò)誤理解的地方,正因?yàn)檫@樣葫掉,我們是不可以使用sync.Pool去實(shí)現(xiàn)一個(gè)socket連接池的些举。

2、緩存對(duì)象的開銷

如何在多個(gè)goroutine之間使用同一個(gè)pool做到高效呢俭厚?官方的做法就是盡量減少競(jìng)爭(zhēng)户魏,因?yàn)閟ync.pool為每個(gè)P(對(duì)應(yīng)cpu,不了解的童鞋可以去看看golang的調(diào)度模型介紹)都分配了一個(gè)子池挪挤,如下圖:


image.png

當(dāng)執(zhí)行一個(gè)pool的get或者put操作的時(shí)候都會(huì)先把當(dāng)前的goroutine固定到某個(gè)P的子池上面绪抛,然后再對(duì)該子池進(jìn)行操作。每個(gè)子池里面有一個(gè)私有對(duì)象和共享列表對(duì)象电禀,私有對(duì)象是只有對(duì)應(yīng)的P能夠訪問,因?yàn)橐粋€(gè)P同一時(shí)間只能執(zhí)行一個(gè)goroutine笤休,因此對(duì)私有對(duì)象存取操作是不需要加鎖的尖飞。共享列表是和其他P分享的,因此操作共享列表是需要加鎖的店雅。

獲取對(duì)象過程是:

1)固定到某個(gè)P政基,嘗試從私有對(duì)象獲取,如果私有對(duì)象非空則返回該對(duì)象闹啦,并把私有對(duì)象置空沮明;

2)如果私有對(duì)象是空的時(shí)候,就去當(dāng)前子池的共享列表獲惹戏堋(需要加鎖)荐健;

3)如果當(dāng)前子池的共享列表也是空的,那么就嘗試去其他P的子池的共享列表偷取一個(gè)(需要加鎖)琳袄;

4)如果其他子池都是空的江场,最后就用用戶指定的New函數(shù)產(chǎn)生一個(gè)新的對(duì)象返回。

可以看到一次get操作最少0次加鎖窖逗,最大N(N等于MAXPROCS)次加鎖址否。

歸還對(duì)象的過程:

1)固定到某個(gè)P,如果私有對(duì)象為空則放到私有對(duì)象碎紊;

2)否則加入到該P(yáng)子池的共享列表中(需要加鎖)佑附。

可以看到一次put操作最少0次加鎖,最多1次加鎖仗考。

由于goroutine具體會(huì)分配到那個(gè)P執(zhí)行是golang的協(xié)程調(diào)度系統(tǒng)決定的音同,因此在MAXPROCS>1的情況下,多goroutine用同一個(gè)sync.Pool的話秃嗜,各個(gè)P的子池之間緩存的對(duì)象是否平衡以及開銷如何是沒辦法準(zhǔn)確衡量的瘟斜。但如果goroutine數(shù)目和緩存的對(duì)象數(shù)目遠(yuǎn)遠(yuǎn)大于MAXPROCS的話缸夹,概率上說應(yīng)該是相對(duì)平衡的。

總的來說螺句,sync.Pool的定位不是做類似連接池的東西虽惭,它的用途僅僅是增加對(duì)象重用的幾率,減少gc的負(fù)擔(dān)蛇尚,而開銷方面也不是很便宜的芽唇。

二、源碼

sync.Pool首先聲明了兩個(gè)結(jié)構(gòu)體

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

type poolLocal struct {
    poolLocalInternal

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

為了使得在多個(gè)goroutine中高效的使用goroutine取劫,sync.Pool為每個(gè)P(對(duì)應(yīng)CPU)都分配一個(gè)本地池匆笤,當(dāng)執(zhí)行Get或者Put操作的時(shí)候,會(huì)先將goroutine和某個(gè)P的子池關(guān)聯(lián)谱邪,再對(duì)該子池進(jìn)行操作炮捧。 每個(gè)P的子池分為私有對(duì)象和共享列表對(duì)象,私有對(duì)象只能被特定的P訪問惦银,共享列表對(duì)象可以被任何P訪問咆课。因?yàn)橥粫r(shí)刻一個(gè)P只能執(zhí)行一個(gè)goroutine,所以無需加鎖书蚪,但是對(duì)共享列表對(duì)象進(jìn)行操作時(shí),因?yàn)榭赡苡卸鄠€(gè)goroutine同時(shí)操作迅栅,所以需要加鎖殊校。

值得注意的是poolLocal結(jié)構(gòu)體中有個(gè)pad成員,目的是為了防止false sharing读存。cache使用中常見的一個(gè)問題是false sharing为流。當(dāng)不同的線程同時(shí)讀寫同一cache line上不同數(shù)據(jù)時(shí)就可能發(fā)生false sharing。false sharing會(huì)導(dǎo)致多核處理器上嚴(yán)重的系統(tǒng)性能下降让簿。具體的可以參考偽共享(false sharing)艺谆,并發(fā)編程無聲的性能殺手

類型sync.Pool有兩個(gè)公開的方法,一個(gè)是Get拜英,一個(gè)是Put, 我們先來看一下Put的源碼静汤。

// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
    if x == nil {
        return
    }
    if race.Enabled {
        if fastrand()%4 == 0 {
            // Randomly drop x on floor.
            return
        }
        race.ReleaseMerge(poolRaceAddr(x))
        race.Disable()
    }
    l := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    runtime_procUnpin()
    if x != nil {
        l.Lock()
        l.shared = append(l.shared, x)
        l.Unlock()
    }
    if race.Enabled {
        race.Enable()
    }
}

1.如果放入的值為空,直接return.
2.檢查當(dāng)前goroutine的是否設(shè)置對(duì)象池私有值居凶,如果沒有則將x賦值給其私有成員虫给,并將x設(shè)置為nil。
3.如果當(dāng)前goroutine私有值已經(jīng)被設(shè)置侠碧,那么將該值追加到共享列表抹估。

func (p *Pool) Get() interface{} {
    if race.Enabled {
        race.Disable()
    }
    l := p.pin()
    x := l.private
    l.private = nil
    runtime_procUnpin()
    if x == nil {
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
        }
        l.Unlock()
        if x == nil {
            x = p.getSlow()
        }
    }
    if race.Enabled {
        race.Enable()
        if x != nil {
            race.Acquire(poolRaceAddr(x))
        }
    }
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

1.嘗試從本地P對(duì)應(yīng)的那個(gè)本地池中獲取一個(gè)對(duì)象值, 并從本地池沖刪除該值。
2.如果獲取失敗弄兜,那么從共享池中獲取, 并從共享隊(duì)列中刪除該值药蜻。
3.如果獲取失敗瓷式,那么從其他P的共享池中偷一個(gè)過來,并刪除共享池中的該值(p.getSlow())语泽。
4.如果仍然失敗贸典,那么直接通過New()分配一個(gè)返回值,注意這個(gè)分配的值不會(huì)被放入池中踱卵。New()返回用戶注冊(cè)的New函數(shù)的值廊驼,如果用戶未注冊(cè)New,那么返回nil惋砂。

三妒挎、總結(jié)

通過以上的解讀,我們可以看到西饵,Get方法并不會(huì)對(duì)獲取到的對(duì)象值做任何的保證酝掩,因?yàn)榉湃氡镜爻刂械闹涤锌赡軙?huì)在任何時(shí)候被刪除,但是不通知調(diào)用者眷柔。放入共享池中的值有可能被其他的goroutine偷走期虾。 所以對(duì)象池比較適合用來存儲(chǔ)一些臨時(shí)切狀態(tài)無關(guān)的數(shù)據(jù),但是不適合用來存儲(chǔ)數(shù)據(jù)庫(kù)連接的實(shí)例闯割,因?yàn)榇嫒雽?duì)象池重的值有可能會(huì)在垃圾回收時(shí)被刪除掉,這違反了數(shù)據(jù)庫(kù)連接池建立的初衷竿拆。

根據(jù)上面的說法宙拉,Golang的對(duì)象池嚴(yán)格意義上來說是一個(gè)臨時(shí)的對(duì)象池,適用于儲(chǔ)存一些會(huì)在goroutine間分享的臨時(shí)對(duì)象丙笋。主要作用是減少GC谢澈,提高性能。在Golang中最常見的使用場(chǎng)景是fmt包中的輸出緩沖區(qū)御板。

在Golang中如果要實(shí)現(xiàn)連接池的效果锥忿,可以用container/list來實(shí)現(xiàn),開源界也有一些現(xiàn)成的實(shí)現(xiàn)怠肋,比如go-commons-pool敬鬓,具體的讀者可以去自行了解。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末笙各,一起剝皮案震驚了整個(gè)濱河市钉答,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杈抢,老刑警劉巖数尿,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惶楼,居然都是意外死亡右蹦,警方通過查閱死者的電腦和手機(jī)诊杆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來何陆,“玉大人晨汹,你說我怎么就攤上這事〖紫祝” “怎么了宰缤?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)晃洒。 經(jīng)常有香客問我慨灭,道長(zhǎng),這世上最難降的妖魔是什么球及? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任氧骤,我火速辦了婚禮,結(jié)果婚禮上吃引,老公的妹妹穿的比我還像新娘筹陵。我一直安慰自己,他們只是感情好镊尺,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布朦佩。 她就那樣靜靜地躺著,像睡著了一般庐氮。 火紅的嫁衣襯著肌膚如雪语稠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天弄砍,我揣著相機(jī)與錄音仙畦,去河邊找鬼。 笑死音婶,一個(gè)胖子當(dāng)著我的面吹牛慨畸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衣式,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼寸士,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了碴卧?” 一聲冷哼從身側(cè)響起碉京,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎螟深,沒想到半個(gè)月后谐宙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡界弧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年凡蜻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搭综。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡划栓,死狀恐怖兑巾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忠荞,我是刑警寧澤蒋歌,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站委煤,受9級(jí)特大地震影響堂油,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碧绞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一府框、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讥邻,春花似錦迫靖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至发魄,卻和暖如春盹牧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欠母。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工欢策, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吆寨,地道東北人赏淌。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像啄清,于是被迫代替她去往敵國(guó)和親六水。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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