sync.pool使用介紹


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM