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對象過程:
- goroutine固定到某一個P后重罪,先從當前子池私區(qū)拿樱哼。并置私有對象為空。
- 拿不到再從當前子池共享列表拿剿配,需要加鎖搅幅。
- 仍拿不到從其它子池共享列表拿,需要加鎖呼胚。
- 仍拿不到茄唐,sync.pool.New閉包非空,則New一個對象蝇更。
- 所以最壞的情況下遍歷其它P才拿到對象沪编,最大值為MACPROCS。
Put過程:
- 固定P中私有對象為空年扩,則放到私有對象漾抬。
- 否則放入當前子池的共享列表,加鎖實現(xiàn)常遂。
- 開銷為最多一次加鎖。
如何解決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