看gin源碼時(shí)發(fā)現(xiàn)了sync.Pool的使用
// gin.go:L144
func New() *Engine {
...
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
// gin.go: L346
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
那個(gè)時(shí)候其實(shí)不太明白這個(gè)Pool是在干啥用, 大致覺得應(yīng)該是內(nèi)存池之類的. 后面想仔細(xì)看下sync.Pool具體怎么用, 我就去直接看了下Pool的源碼, 然后直接懵逼了
因?yàn)榛究床欢溥壿? 因?yàn)镻ool的源碼涉及到Golang的調(diào)度相關(guān)的知識(shí). 要是不明白Golang是如何調(diào)度的, 基本是看不懂Pool的源碼的(雖說它只有短短的259行代碼, 文件路徑: SDK/src/sync/pool.go). 這里推薦Go調(diào)度器系列系列文章
后面我就去看看別人是怎么理解和使用Pool的, 我就搜到了這么一篇文章 go語言的官方包sync.Pool的實(shí)現(xiàn)原理和適用場(chǎng)景里面有這么一個(gè)例子
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)
}
以及Golang中國論壇上一個(gè)人的提問, 我不禁陷入了深深的疑問, 這個(gè)Golang的Pool到底是用來做什么的?
我疑問的地方:
- sync.Pool的運(yùn)用場(chǎng)景是什么, 哪些地方能用到Pool, 哪些地方不能用?
- 為啥CSDN上先Put后Get, 就能拿到1; 而Golang中國上那個(gè)提問, 為啥得到的順序就不確定了?
我后面想了很久, 以及再回頭去看gin, logrus的源碼, 我才想明白: 這兩個(gè)例子都是"坑貨". 這兩個(gè)例子都是sync.Pool使用的反面例子, 都是不正確的用法. 其實(shí)這些在源碼的注釋中, 都是由明確說明的, 只是當(dāng)時(shí)沒能理解. Callers should not assume any relation between values passed to Put and the values returned by Get. 簡單來說: 就是Get和Put沒有任何關(guān)系, 我們用Pool的時(shí)候, 要時(shí)刻記得這個(gè).
因此, 我的疑問2就迎刃而解了, 其實(shí)就是這個(gè)一直困惑著我
關(guān)于疑問1就更簡單了
// 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.
簡單來說, Pool就是為了減少GC壓力的, 重復(fù)利用內(nèi)存. 千萬不能把他當(dāng)成內(nèi)存池使用
其實(shí)Pool的用法很簡單, 就是先Get, 用完之后Put, 如gin的使用.
再比如logrus的用法
func (logger *Logger) Println(args ...interface{}) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) newEntry() *Entry {
entry, ok := logger.entryPool.Get().(*Entry)
if ok {
return entry
}
return NewEntry(logger)
}
func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
logger.entryPool.Put(entry)
}
所以別被別的池子帶跑了, golang里的sync.Pool就是GC優(yōu)化的, 用法很簡單
在gocn有這么一個(gè)問題: 想在pool的基礎(chǔ)上做一個(gè)限制池中對(duì)象數(shù)量的功能, 發(fā)現(xiàn)還是多次執(zhí)行pool.New. 期望是只執(zhí)行一次NewBuffer,也就是只打印一次alloc。 實(shí)際上每次執(zhí)行,會(huì)打印多次alloc
const MaxFrameSize = 5000
func main() {
for i := 0; i < 10; i++ {
// 多個(gè)協(xié)程想從pool中拿到對(duì)象
go func() {
c := getBuf()
putBuf(c)
log.Println("put done")
}()
}
time.Sleep(3 * time.Second)
}
var bufPool = sync.Pool{
New: func() interface{} {
log.Println("alloc")
return bytes.NewBuffer(make([]byte, 0, MaxFrameSize))
},
}
var bufPoolChan = make(chan bool, 1)
func getBuf() *bytes.Buffer {
bufPoolChan <- true
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
return b
}
func putBuf(b *bytes.Buffer) {
bufPool.Put(b)
<-bufPoolChan
}
大神解答:
sync.Pool的源代碼里說了,pool里的對(duì)象隨時(shí)都有可能被自動(dòng)移除,并且沒有任何通知。sync.Pool的數(shù)量是不可控制的。
Pool調(diào)用New與線程調(diào)度有關(guān),Pool內(nèi)部有一個(gè)localPool的數(shù)組,每個(gè)P對(duì)應(yīng)其中一個(gè)localPool,在當(dāng)前P執(zhí)行g(shù)oroutine的時(shí)候,優(yōu)先從當(dāng)前的localPool的private變量取,娶不到在從shared列表里面取,再取不到就嘗試從別的P的localPool的shared里面偷一個(gè)。最后實(shí)在取不到就New一個(gè)。
由于你的bufPoolChan限制基本上10個(gè)goroutine就在兩個(gè)P后面排隊(duì)輪流執(zhí)行,所以alloc就會(huì)出現(xiàn)兩次,后面的基本就是從這兩個(gè)localPool的private取出來的。
如果取消這個(gè)限制,10個(gè)goroutine很快就被分配到10個(gè)P上去了,對(duì)應(yīng)就有10個(gè)localPool,10次每次取private都取不到,取shared列表也取不到,別的localPool也沒得偷,就會(huì)New10次,alloc就會(huì)出現(xiàn)10次。
其他高級(jí)用法, 后面再補(bǔ)充
參考文章
[譯] CockroachDB GC優(yōu)化總結(jié)
Golang 優(yōu)化之路——臨時(shí)對(duì)象池
用Benchmark驗(yàn)證sync.Pool對(duì)GC latency的優(yōu)化效果
Go語言實(shí)戰(zhàn)筆記(十六)| Go 并發(fā)示例-Pool
Pool