1. 簡介
作用:頻繁地分配、回收內存會給 GC 帶來一定的負擔,嚴重的時候會引起 CPU 的毛刺,而 sync.Pool 可以將暫時不用的對象緩存起來,待下次需要的時候直接使用,不用再次經過內存分配,復用對象的內存,減輕 GC 的壓力,提升系統的性能。
為了使得在多個goroutine中高效的使用goroutine,sync.Pool為每個P(對應CPU)都分配一個本地池,
當執行Get或者Put操作的時候,會先將goroutine和某個P的子池關聯,再對該子池進行操作。
每個P的子池分為私有對象和共享列表對象,私有對象只能被特定的P訪問,共享列表對象可以被任何P訪問。
因為同一時刻一個P只能執行一個goroutine,所以無需加鎖,但是對共享列表對象進行操作時,
因為可能有多個goroutine同時操作,所以需要加鎖。
Put方法:
如果放入的值為空,直接return.
檢查當前goroutine的是否設置對象池私有值,如果沒有則將x賦值給其私有成員,並將x設置為nil。
如果當前goroutine私有值已經被設置,那么將該值追加到共享列表。
Get方法:
嘗試從本地P對應的那個本地池中獲取一個對象值, 並從本地池沖刪除該值。
如果獲取失敗,那么從共享池中獲取, 並從共享隊列中刪除該值。
如果獲取失敗,那么從其他P的共享池中偷一個過來,並刪除共享池中的該值(p.getSlow())。
如果仍然失敗,那么直接通過New()分配一個返回值,注意這個分配的值不會被放入池中。
New()返回用戶注冊的New函數的值,如果用戶未注冊New,那么返回nil。
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
可以看到在init的時候會向GC注冊一個PoolCleanup函數,他會清除掉sync.Pool中的所有的緩存的對象,
這個注冊函數會在每次GC的時候運行,所以sync.Pool中的值只在兩次GC中間的時段有效。
通過以上的解讀,我們可以看到,Get方法並不會對獲取到的對象值做任何的保證,
因為放入本地池中的值有可能會在任何時候被刪除,但是不通知調用者。放入共享池中的值有可能被其他的goroutine偷走。
所以對象池比較適合用來存儲一些臨時切狀態無關的數據,但是不適合用來存儲數據庫連接的實例,
因為存入對象池重的值有可能會在垃圾回收時被刪除掉,這違反了數據庫連接池建立的初衷。
根據上面的說法,Golang的對象池嚴格意義上來說是一個臨時的對象池,適用於儲存一些會在goroutine間分享的臨時對象。主要作用是減少GC,提高性能。
其實sync.pool有點像python線程間共享變量的實現,不同線程間共享變量來實現對象的復用。
2. 使用場景
舉個簡單的例子:
type Student struct {
Name string
Age int32
Remark [1024]byte
}
var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25})
func unmarsh() {
stu := &Student{}
json.Unmarshal(buf, stu)
}
json 的反序列化在文本解析和網絡通信過程中非常常見,當程序並發度非常高的情況下,短時間內需要創建大量的臨時對象。而這些對象是都是分配在堆上的,會給 GC 造成很大壓力,嚴重影響程序的性能。
3. 如何使用
sync.Pool 的使用方式非常簡單:
3.1 聲明對象池
只需要實現 New 函數即可。對象池中沒有對象時,將會調用 New 函數創建。
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
3.2 Get & Put
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
- Get()用於從對象池中獲取對象,因為返回值是interface{}`,因此需要類型轉換。
- Put()` 則是在對象使用完畢后,返回對象池。
4. 性能測試
4.1 struct 反序列化
func BenchmarkUnmarshal(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := &Student{}
json.Unmarshal(buf, stu)
}
}
func BenchmarkUnmarshalWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
}
}
var stu = new(Student)
func BenchmarkUnmarshalWithGlobal(b *testing.B) {
for n := 0; n < b.N; n++ {
json.Unmarshal(buf, stu)
}
}
測試結果如下:
$ go test -bench="^BenchmarkUnmar" -benchmem
goos: darwin
goarch: amd64
pkg: example/hpg-sync-pool
BenchmarkUnmarshal-8 9277 131042 ns/op 1384 B/op 7 allocs/op
BenchmarkUnmarshalWithPool-8 9441 130150 ns/op 232 B/op 6 allocs/op
BenchmarkUnmarshalWithGlobal-8 9860 136932 ns/op 232 B/op 6 allocs/op
PASS
ok example/hpg-sync-pool 2.334s
在這個例子中,因為 Student 結構體內存占用較小,內存分配幾乎不耗時間。而標准庫 json 反序列化時利用了反射,效率是比較低的,占據了大部分時間,因此兩種方式最終的執行時間幾乎沒什么變化。但是內存占用差了一個數量級,第一種沒有使用池,每執行一次用例都比后兩者多一次內存分配,而第二個使用了 sync.Pool 后,內存占用僅為未使用的 234/5096 = 1/22,對 GC 的影響就很大了。第三個因為引入了全局變量,也不會產生內存分配,故而和第二個用例消耗性能差不多
4.2 bytes.Buffer
var bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var data = make([]byte, 10000)
func BenchmarkBufferWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Write(data)
buf.Reset()
bufferPool.Put(buf)
}
}
func BenchmarkBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
var buf bytes.Buffer
buf.Write(data)
}
}
測試結果如下:
$ go test -bench="^BenchmarkBuffer" -run=none -benchmem
goos: linux
goarch: amd64
pkg: github.com/go-test/benckmark
BenchmarkBufferWithPool-8 8313493 141 ns/op 0 B/op 0 allocs/op
BenchmarkBuffer-8 795733 1321 ns/op 10240 B/op 1 allocs/op
PASS
ok github.com/go-test/benckmark 3.408s
這個例子創建了一個 bytes.Buffer 對象池,而且每次只執行一個簡單的 Write 操作,存粹的內存搬運工,耗時幾乎可以忽略。而內存分配和回收的耗時占比較多,因此對程序整體的性能影響更大。
5. 在標准庫中的應用
5.1 fmt.Printf
Go 語言標准庫也大量使用了 sync.Pool,例如 fmt 和 encoding/json。
以下是 fmt.Printf 的源代碼(go/src/fmt/print.go):
// go 1.13.6
// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
buf buffer
...
}
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
// free saves used pp structs in ppFree; avoids an allocation per invocation.
func (p *pp) free() {
if cap(p.buf) > 64<<10 {
return
}
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
fmt.Printf 的調用是非常頻繁的,利用 sync.Pool 復用 pp 對象能夠極大地提升性能,減少內存占用,同時降低 GC 壓力。
這個例子來源於:深度解密 Go 語言之 sync.Pool