golang sync.pool對象復用 并發(fā)原理 緩存池

golang sync.pool對象復用 并發(fā)原理 緩存池

在go http每一次go serve(l)都會構建Request數(shù)據結構泊窘。在大量數(shù)據請求或高并發(fā)的場景中,頻繁創(chuàng)建銷毀對象郭蕉,會導致GC壓力。解決辦法之一就是使用對象復用技術喂江。在http協(xié)議層之下召锈,使用對象復用技術創(chuàng)建Request數(shù)據結構。在http協(xié)議層之上获询,可以使用對象復用技術創(chuàng)建(w,*r,ctx)數(shù)據結構涨岁。這樣即可以回快TCP層讀包之后的解析速度,也可也加快請求處理的速度吉嚣。

先上一個測試:

//測試平臺 i5 3.8GHz 4核
    bPool := sync.Pool{
        New: func() interface{} {
            b := make([]byte,1024)
            return &b
        },
    }

    t1 := time.Now().Unix()
    count := 1000000000
    for i:=0;i<count;i++{
        buf := make([]byte,1024)
        _ = buf
    }
    t2 := time.Now().Unix()
    for i:=0;i<count;i++{
        buf := bPool.Get().(*[]byte)
        _ = buf
        //clear buf
        bPool.Put(buf)
    }
    t3 := time.Now().Unix()
    fmt.Println("new:%d s",t2-t1)
    fmt.Println("pool:%d s",t3-t1)

結論是這樣的:

new:%d s 21
pool:%d s 396

貌似使用池化梢薪,性能弱爆了?尝哆?秉撇?這似乎與net/http使用sync.pool池化Request來優(yōu)化性能的選擇相違背。這同時也說明了一個問題秋泄,好的東西琐馆,如果濫用反而造成了性能成倍的下降。在看過pool原理之后恒序,結合實例瘦麸,將給出正確的使用方法,并給出預期的效果歧胁。

基本用法

sync.Pool是一個協(xié)程安全臨時對象池滋饲。數(shù)據結構如下:

type Pool struct {
    noCopy noCopy // type noCopy struct{}

    local     unsafe.Pointer 
    localSize uintptr        

    New func() interface{}
}
type poolLocal struct {  
    private interface{}   // Can be used only by the respective P.  
    shared  []interface{} // Can be used by any P.  
    Mutex                 // Protects shared.  
    pad     [128]byte     // Prevents false sharing.  
} 

local 成員的真實類型是一個 poolLocal 數(shù)組厉碟,localSize 是數(shù)組長度。這涉及到Pool實現(xiàn)了赌,pool為每個P分配了一個對象墨榄,P數(shù)量設置為runtime.GOMAXPROCS(0)。在并發(fā)讀寫時勿她,goroutine綁定的P有對象袄秩,先用自己的,沒有去偷其它P的逢并。go語言將數(shù)據分散在了各個真正運行的P中之剧,降低了鎖競爭,提高了并發(fā)能力砍聊。

不要習慣性地誤認為New是一個關鍵字背稼,這里的New是Pool的一個字段,也是一個閉包名稱玻蝌。其API:

var pool = &sync.Pool{New:func()interface{}{return NewObject()}}
//池對象最好初始化為全局唯一
pool.Put(x interface{})
pool.Get() interface{}

如果不指定New字段蟹肘,對象池為空時會返回nil,而不是一個新構建的對象俯树。Get()到的對象是隨機的帘腹。

    pool := sync.Pool{New: func() interface{} {
        return "empty string"
    }}
    s := "Hello World"
    pool.Put(s)
    fmt.Println(pool.Get())
    fmt.Println(pool.Get())

一個緩存池的例子

type BufPool struct {
    pool sync.Pool
    spliter string
}

func NewBufPool() *BufPool {
    return &BufPool{
        pool:sync.Pool{
            New: func() interface{} {
                return &bytes.Buffer{}
            },
        },
        spliter:" ",
    }
}

func (this *BufPool)JoinString(strs ...string) (res string,err error) {
    if len(strs) == 0 {
        return
    }

    buf := this.pool.Get().(*bytes.Buffer)
    if _,err := buf.WriteString(strs[0]);err!=nil{
        return "",err
    }
    for _,str := range strs[1:] {
        if _,err := buf.WriteString(this.spliter);err!= nil {
            return "",err
        }
        if _,err := buf.WriteString(str);err!= nil {
            return "",err
        }
    }

    res = buf.String()
    buf.Reset()
    this.pool.Put(buf)
    return 
}

原生sync.Pool的問題是,Pool中的對象會被GC清理掉许饿,這使得sync.Pool只適合做簡單地對象池阳欲,不適合作連接池。

為何不適合作連接池

對象的數(shù)量和期限

pool創(chuàng)建時不能指定大小陋率,沒有數(shù)量限制球化。pool中對象會被GC清掉,只存在于兩次GC之間瓦糟。實現(xiàn)是pool的init方法注冊了一個poolCleanup()函數(shù)筒愚,這個方法在GC之前執(zhí)行,清空pool中的所有緩存對象狸页。

池對象Get/Put開銷

為使多協(xié)程使用同一個POOL锨能。最基本的想法就是每個協(xié)程,加鎖去操作共享的POOL芍耘,這顯然是低效的址遇。而進一步改進,類似于ConcurrentHashMap(JDK7)的分Segment斋竞,提高其并發(fā)性可以一定程度性緩解倔约。

注意到pool中的對象是無差異性的,加鎖或者分段加鎖都不是較好的做法坝初。go的做法是為每一個綁定協(xié)程的P都分配一個子池浸剩。每個子池又分為私有池和共享列表钾军。共享列表是分別存放在各個P之上的共享區(qū)域,而不是各個P共享的一塊內存绢要。協(xié)程拿自己P里的子池對象不需要加鎖吏恭,拿共享列表中的就需要加鎖了。

Get對象過程:

  1. goroutine固定到某一個P后重罪,先從當前子池私區(qū)拿樱哼。并置私有對象為空。
  2. 拿不到再從當前子池共享列表拿剿配,需要加鎖搅幅。
  3. 仍拿不到從其它子池共享列表拿,需要加鎖呼胚。
  4. 仍拿不到茄唐,sync.pool.New閉包非空,則New一個對象蝇更。
  5. 所以最壞的情況下遍歷其它P才拿到對象沪编,最大值為MACPROCS。

Put過程:

  1. 固定P中私有對象為空年扩,則放到私有對象漾抬。
  2. 否則放入當前子池的共享列表,加鎖實現(xiàn)常遂。
  3. 開銷為最多一次加鎖。

如何解決Get最壞情況遍歷所有P才獲取得對象呢:

  • 能夠設置加鎖期間遍歷其它P的最大次數(shù)挽荠,遍歷不到就直接創(chuàng)建克胳,減少加鎖占用pool的時間。
  • 使各子池共享列表中的對象數(shù)量盡量平均化圈匆,從而避免最壞的情況發(fā)生漠另。

方法1止前sync.pool并沒有這樣的設置。方法2由于goroutine被分配到哪個P由調度器調度不可控跃赚,無法確保其平衡笆搓。

由于不可控的GC導致生命周期過短,且池大小不可控纬傲,因而不適合作連接池满败。僅適用于增加對象重用機率,減少GC負擔叹括。2

用實驗回答篇頭的問題

實驗1

var bytePool = sync.Pool{
   New: func() interface{} {
      b := make([]byte, 1024)
      return &b
   },
}

func main() {
   runtime.GOMAXPROCS(16)
   a := time.Now().Unix()
   count := 100000000
   // 不使用對象池
   for i := 0; i < 1; i++ {
      for j:=0;j<count;j++{
         obj := make([]byte, 1024)
         _ = obj
      }
   }
   b := time.Now().Unix()

   c := time.Now().Unix()
   // 使用對象池
   for i := 0; i < 1; i++ {
      go func() {
         for j := 0; j < count; j++ {
            obj := bytePool.Get().(*[]byte)
            _ = obj
            bytePool.Put(obj)
         }
      }()
   }
   d := time.Now().Unix()
   fmt.Println("without pool ", b-a, "s")
   fmt.Println("with    pool ", d-c, "s")
}

執(zhí)行結果:

without pool  2 s
with    pool  0 s

單線程情況下算墨,遍歷其它無元素的P,長時間加鎖性能低下汁雷。啟用協(xié)程改善净嘀。

實驗2

var bytePool = sync.Pool{
   New: func() interface{} {
      b := make([]byte, 1024)
      return &b
   },
}

func main() {
   runtime.GOMAXPROCS(16)
   a := time.Now().Unix()
   count := 100000000
   // 不使用對象池
   for i := 0; i < 1000; i++ {
      for j:=0;j<count;j++{
         obj := make([]byte, 1024)
         _ = obj
      }
   }
   b := time.Now().Unix()

   c := time.Now().Unix()
   // 使用對象池
   for i := 0; i < 1000; i++ {
      go func() {
         for j := 0; j < count; j++ {
            obj := bytePool.Get().(*[]byte)
            _ = obj
            bytePool.Put(obj)
         }
      }()
   }
   d := time.Now().Unix()
   fmt.Println("without pool ", b-a, "s")
   fmt.Println("with    pool ", d-c, "s")
}

結果:

without pool  2000 s
with    pool  2 s

測試場景在goroutines遠大于GOMAXPROCS情況下报咳,與非池化性能差異巨大。

實驗3

var bytePool1 = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 1024)
        return &b
    },
}

var bytePool2 = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 1024)
        return &b
    },
}

func main() {
    runtime.GOMAXPROCS(4)
    a := time.Now().Unix()
    count := 1000000000
    goCount := 1000
    for i := 0; i < goCount; i++ {
        go func() {
            for j := 0; j < count; j++ {
                obj := bytePool1.Get().(*[]byte)
                _ = obj
                bytePool1.Put(obj)
            }
        }()
    }
    b := time.Now().Unix()

    for i := 0; i < 1000; i++ {
        go func() {
            for j := 0; j < count; j++ {
                bNew := make([]byte, 1024)
                bytePool2.Put(&bNew)
            }
        }()
    }
    c := time.Now().Unix()
    for i := 0; i < goCount; i++ {
        go func() {
            for j := 0; j < count; j++ {
                obj := bytePool2.Get().(*[]byte)
                _ = obj
                bytePool2.Put(obj)
            }
        }()
    }
    d := time.Now().Unix()
    fmt.Println("without pool ", b-a, "s")
    fmt.Println("with    pool ", d-c, "s")
}

測試結果

without pool  6 s
with    pool  0 s

可以看到同樣使用*sync.pool挖藏,較大池大小的命中率較高暑刃,性能遠高于空池。

結論:pool在一定的使用條件下提高并發(fā)性能膜眠,條件1是協(xié)程數(shù)遠大于GOMAXPROCS岩臣,條件2是池中對象遠大于GOMAXPROCS。歸結成一個原因就是使對象在各個P中均勻分布柴底。

關于何時回收Pool

池pool和緩存cache的區(qū)別婿脸。池的意思是,池內對象是可以互換的柄驻,不關心具體值狐树,甚至不需要區(qū)分是新建的還是從池中拿出的。緩存指的是KV映射鸿脓,緩存里的值互不相同抑钟,清除機制更為復雜。緩存清除算法如LRU野哭、LIRS緩存算法在塔。

池空間回收的幾種方式。一些是GC前回收拨黔,一些是基于時鐘或弱引用回收蛔溃。最終確定在GC時回收Pool內對象,即不回避GC篱蝇。用java的GC解釋弱引用贺待。GC的四種引用:強引用、弱引用零截、軟引用麸塞、虛引用。虛引用即沒有引用涧衙,弱引用GC但有空間則保留哪工,軟引用GC即清除。ThreadLocal的值為弱引用的例子弧哎。

Pool其它場景

regexp包為了保證并發(fā)時使用同一個正則雁比,而維護了一組狀態(tài)機。

fmt包做字串拼接撤嫩,從sync.pool拿[]byte對象章贞。避免頻繁構建再GC效率高很多。

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

https://blog.csdn.net/qq_33339479/article/details/64116948

https://blog.csdn.net/bravezhe/article/details/79887514

https://www.cnblogs.com/hetonghai/p/9086788.html

http://www.reibang.com/p/2bd41a8f2254

https://www.cnblogs.com/hump/p/6285627.html

https://www.cnblogs.com/DaBing0806/p/6934318.html

https://studygolang.com/articles/700

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鸭限,隨后出現(xiàn)的幾起案子蜕径,更是在濱河造成了極大的恐慌,老刑警劉巖败京,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兜喻,死亡現(xiàn)場離奇詭異,居然都是意外死亡赡麦,警方通過查閱死者的電腦和手機朴皆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泛粹,“玉大人遂铡,你說我怎么就攤上這事【фⅲ” “怎么了扒接?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長们衙。 經常有香客問我钾怔,道長,這世上最難降的妖魔是什么蒙挑? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任宗侦,我火速辦了婚禮,結果婚禮上忆蚀,老公的妹妹穿的比我還像新娘矾利。我一直安慰自己,他們只是感情好馋袜,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布梦皮。 她就那樣靜靜地躺著,像睡著了一般桃焕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捧毛,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天幽勒,我揣著相機與錄音曙砂,去河邊找鬼。 笑死,一個胖子當著我的面吹牛罩阵,可吹牛的內容都是我干的。 我是一名探鬼主播性雄,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼留拾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泞辐?” 一聲冷哼從身側響起笔横,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竞滓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吹缔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體商佑,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年厢塘,在試婚紗的時候發(fā)現(xiàn)自己被綠了茶没。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡晚碾,死狀恐怖抓半,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情格嘁,我是刑警寧澤笛求,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站讥蔽,受9級特大地震影響涣易,放射性物質發(fā)生泄漏。R本人自食惡果不足惜冶伞,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一新症、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧响禽,春花似錦徒爹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至侯繁,卻和暖如春胖喳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贮竟。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工丽焊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咕别。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓技健,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惰拱。 傳聞我的和親對象是個殘疾皇子雌贱,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容