golang对象池
簡介
-
sync.Pool是一個可以存或取的臨時對象集合
-
sync.Pool可以安全被多個線程同時使用,保證線程安全
-
注意、注意、注意,sync.Pool中保存的任何項都可能隨時不做通知的釋放掉,所以不適合用于像socket長連接或數(shù)據庫連接池。
-
sync.Pool主要用途是增加臨時對象的重用率,減少GC負擔
關于堆和棧
程序會從操作系統(tǒng)申請一塊內存,而這塊內存也會被分成堆和棧。棧可以簡單得理解成一次函數(shù)調用內部申請到的內存,它們會隨著函數(shù)的返回把內存還給系統(tǒng)。
func F() {temp := make([]int, 0, 20)... }類似于上面代碼里面的temp變量,只是內函數(shù)內部申請的臨時變量,并不會作為返回值返回,它就是被編譯器申請到棧里面。申請到棧內存好處:函數(shù)返回直接釋放,不會引起垃圾回收,對性能沒有影響。
func F() []int{a := make([]int, 0, 20)return a }而上面這段代碼,申請的代碼一模一樣,但是申請后作為返回值返回了,編譯器會認為變量之后還會被使用,當函數(shù)返回之后并不會將其內存歸還,那么它就會被申請到堆上面了。申請到堆上面的內存才會引起垃圾回收。
func F() {a := make([]int, 0, 20)b := make([]int, 0, 20000)l := 20c := make([]int, 0, l) }a和b代碼一樣,就是申請的空間不一樣大,但是它們兩個的命運是截然相反的。a前面已經介紹過,會申請到棧上面,而b,由于申請的內存較大,編譯器會把這種申請內存較大的變量轉移到堆上面。即使是臨時變量,申請過大也會在堆上面申請。
而c,對我們而言其含義和a是一致的,但是編譯器對于這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。
實際項目基本都是通過c := make([]int, 0, l)來申請內存,長度都是不確定的。自然而然這些變量都會申請到堆上面了
簡單得說,就是程序要從操作系統(tǒng)申請一塊比較大的內存,內存分成小塊,通過鏈表鏈接。每次程序申請內存,就從鏈表上面遍歷每一小塊,找到符合的就返回其地址,沒有合適的就從操作系統(tǒng)再申請。如果申請內存次數(shù)較多,而且申請的大小不固定,就會引起內存碎片化的問題。申請的堆內存并沒有用完,但是用戶申請的內存的時候卻沒有合適的空間提供。這樣會遍歷整個鏈表,還會繼續(xù)向操作系統(tǒng)申請內存。這就能解釋我一開始描述的問題,申請一塊內存變成了慢語句。
申請內存變成了慢語句,解決方法就是使用臨時對象池
臨時對象池
如何解決這個問題,首先想到的就是對象池。Golang在sync里面提供了對象池Pool。一般大家都叫這個為對象池,而我喜歡叫它臨時對象池。因為每次垃圾回收會把池子里面不被引用的對象回收掉。
func (p *Pool) Get() interface{}
需要注意的是,Get方法會把返回的對象從池子里面刪除。所以用完了的對象,還是得重新放回池子
package mainimport ("fmt""sync""time" )// 一個[]byte的對象池,每個對象為一個[]byte var bytePool = sync.Pool{New: func() interface{} {b := make([]byte, 1024)return &b}, }func main() {a := time.Now().Unix()// 不使用對象池for i := 0; i < 1000000000; i++ {obj := make([]byte, 1024)_ = obj}b := time.Now().Unix()// 使用對象池for i := 0; i < 1000000000; i++ {obj := bytePool.Get().(*[]byte)bytePool.Put(obj)}c := time.Now().Unix()fmt.Println("without pool ", b-a, "s")fmt.Println("with pool ", c-b, "s") }輸出
without pool 20 s with pool 15 s package mainimport ("fmt""sync" )// 一個[]byte的對象池,每個對象為一個[]byte var bytePool = sync.Pool{New: func() interface{} {b := make([]byte, 8)return &b}, }func main() {fmt.Printf("%T\n", bytePool)fmt.Printf("%+v\n", bytePool)obj := bytePool.Get().(*[]byte)fmt.Printf("%T\n", obj)fmt.Printf("%v\n", obj) }輸出
sync.Pool {noCopy:{} local:<nil> localSize:0 New:0x1090180} *[]uint8 &[0 0 0 0 0 0 0 0]何時使用pool
只有當每個對象占用內存較大時候,用pool才會改善性能
對比1(起步階段):
package mainimport ("fmt""sync""time" )// 一個[]byte的對象池,每個對象為一個[]byte var bytePool = sync.Pool{New: func() interface{} {b := make([]byte, 1)return &b}, }func main() {a := time.Now().Unix()// 不使用對象池for i := 0; i < 1000000000; i++ {obj := make([]byte, 1)_ = obj}b := time.Now().Unix()// 使用對象池for i := 0; i < 1000000000; i++ {obj := bytePool.Get().(*[]byte)bytePool.Put(obj)}c := time.Now().Unix()fmt.Println("without pool ", b-a, "s")fmt.Println("with pool ", c-b, "s") }輸出
without pool 0 s with pool 17 s可以看到,當[]byte只有1個元素時候,用pool性能反而更差
對比2(追趕階段):
package mainimport ("fmt""sync""time" )// 一個[]byte的對象池,每個對象為一個[]byte var bytePool = sync.Pool{New: func() interface{} {b := make([]byte, 800)return &b}, }func main() {a := time.Now().Unix()// 不使用對象池for i := 0; i < 1000000000; i++ {obj := make([]byte, 800)_ = obj}b := time.Now().Unix()// 使用對象池for i := 0; i < 1000000000; i++ {obj := bytePool.Get().(*[]byte)bytePool.Put(obj)}c := time.Now().Unix()fmt.Println("without pool ", b-a, "s")fmt.Println("with pool ", c-b, "s") }輸出
without pool 16 s with pool 17 s可以看到,飛機快趕上跑車了
對比3(超越階段):
package mainimport ("fmt""sync""time" )// 一個[]byte的對象池,每個對象為一個[]byte var bytePool = sync.Pool{New: func() interface{} {b := make([]byte, 8000)return &b}, }func main() {a := time.Now().Unix()// 不使用對象池for i := 0; i < 1000000000; i++ {obj := make([]byte, 8000)_ = obj}b := time.Now().Unix()// 使用對象池for i := 0; i < 1000000000; i++ {obj := bytePool.Get().(*[]byte)bytePool.Put(obj)}c := time.Now().Unix()fmt.Println("without pool ", b-a, "s")fmt.Println("with pool ", c-b, "s") }輸出
without pool 128 s with pool 17 s可以看到2個特征
當每個對象的內存小于一定量的時候,不使用pool的性能秒殺使用pool;當內存處于某個量的時候,不使用pool和使用pool性能相當;當內存大于某個量的時候,使用pool的優(yōu)勢就顯現(xiàn)出來了
不使用pool,那么對象占用內存越大,性能下降越厲害;使用pool,無論對象占用內存大還是小,性能都保持不變。可以看到pool有點像飛機,雖然起步比跑車慢,但后勁十足。
即:pool適合占用內存大且并發(fā)量大的場景。當內存小并發(fā)量少的時候,使用pool適得其反
知識點
package mainimport ("fmt""sync" )// 一個[]int的對象池,每個對象為一個[]int var intPool = sync.Pool{New: func() interface{} {b := make([]int, 8)return &b}, }func main() {// 不使用對象池for i := 1; i < 3; i++ {obj := make([]int, 8)obj[i] = ifmt.Printf("obj%d: %T %+v\n", i, obj, obj)}fmt.Println("-----------")// 使用對象池for i := 1; i < 3; i++ {obj := intPool.Get().(*[]int)(*obj)[i] = ifmt.Printf("obj%d: %T %+v\n", i, obj, obj)intPool.Put(obj)} }輸出
obj1: []int [0 1 0 0 0 0 0 0] obj2: []int [0 0 2 0 0 0 0 0] ----------- obj1: *[]int &[0 1 0 0 0 0 0 0] obj2: *[]int &[0 1 2 0 0 0 0 0]可以看到,pool的Get和Put真的是從池里獲得和放入池里,否則不會出現(xiàn)Get獲得的變量是舊的變量(即之前通過Put放入的)
如果把上面代碼中的intPool.Put(obj)這行刪掉,那么輸出就是
obj1: []int [0 1 0 0 0 0 0 0] obj2: []int [0 0 2 0 0 0 0 0] ----------- obj1: *[]int &[0 1 0 0 0 0 0 0] obj2: *[]int &[0 0 2 0 0 0 0 0]Pool的目的是緩存已分配但未使用的項目以備后用
多協(xié)程并發(fā)安全
緩存在Pool里的item會沒有任何通知情況下隨時被移除,以緩解GC壓力
池提供了一種方法來緩解跨多個客戶端的分配開銷。
不是所有場景都適合用Pool,如果釋放鏈表是某個對象的一部分,并由這個對象維護,而這個對象只由一個客戶端使用,在這個客戶端工作完成后釋放鏈表,那么用Pool實現(xiàn)這個釋放鏈表是不合適的。
官方對Pool的目的描述:
Pool設計用意是在全局變量里維護的釋放鏈表,尤其是被多個 goroutine 同時訪問的全局變量。使用Pool代替自己寫的釋放鏈表,可以讓程序運行的時候,在恰當?shù)膱鼍跋聫某乩镏赜媚稠椫怠ync.Pool一種合適的方法是,為臨時緩沖區(qū)創(chuàng)建一個池,多個客戶端使用這個緩沖區(qū)來共享全局資源。另一方面,如果釋放鏈表是某個對象的一部分,并由這個對象維護,而這個對象只由一個客戶端使用,在這個客戶端工作完成后釋放鏈表,那么用Pool實現(xiàn)這個釋放鏈表是不合適的。
Pool的正確用法
在Put之前重置,在Get之后重置
總結
- 上一篇: golang中的定时器
- 下一篇: golang中的sync.once