[TOC]
# 簡介
* `sync.Pool是一個可以存或取的臨時對象集合`
* `sync.Pool可以安全被多個線程同時使用,保證線程安全`
* `注意、注意、注意,sync.Pool中保存的任何項都可能隨時不做通知的釋放掉,所以不適合用于像socket長連接或數據庫連接池。`
* `sync.Pool主要用途是增加臨時對象的重用率,減少GC負擔`
# 關于堆和棧
程序會從操作系統申請一塊內存,而這塊內存也會被分成堆和棧。棧可以簡單得理解成一次函數調用內部申請到的內存,它們會隨著函數的返回把內存還給系統。
~~~
func F() {
temp := make([]int, 0, 20)
...
}
~~~
類似于上面代碼里面的temp變量,只是內函數內部申請的臨時變量,并不會作為返回值返回,它就是被編譯器申請到棧里面。**申請到棧內存好處:函數返回直接釋放,不會引起垃圾回收,對性能沒有影響。**
~~~
func F() []int{
a := make([]int, 0, 20)
return a
}
~~~
而上面這段代碼,申請的代碼一模一樣,但是申請后作為返回值返回了,編譯器會認為變量之后還會被使用,當函數返回之后并不會將其內存歸還,那么它就會被申請到堆上面了。**申請到堆上面的內存才會引起垃圾回收。**
~~~
func F() {
a := make([]int, 0, 20)
b := make([]int, 0, 20000)
l := 20
c := make([]int, 0, l)
}
~~~
a和b代碼一樣,就是申請的空間不一樣大,但是它們兩個的命運是截然相反的。a前面已經介紹過,會申請到棧上面,而b,由于申請的內存較大,**編譯器會把這種申請內存較大的變量轉移到堆上面。即使是臨時變量,申請過大也會在堆上面申請。**
而c,對我們而言其含義和a是一致的,**但是編譯器對于這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。**
實際項目基本都是通過c := make(\[\]int, 0, l)來申請內存,長度都是不確定的。自然而然這些變量都會申請到堆上面了
簡單得說,就是程序要從操作系統申請一塊比較大的內存,內存分成小塊,通過鏈表鏈接。每次程序申請內存,就從鏈表上面遍歷每一小塊,找到符合的就返回其地址,沒有合適的就從操作系統再申請。如果申請內存次數較多,而且申請的大小不固定,就會引起內存碎片化的問題。申請的堆內存并沒有用完,但是用戶申請的內存的時候卻沒有合適的空間提供。這樣會遍歷整個鏈表,還會繼續向操作系統申請內存。這就能解釋我一開始描述的問題,**申請一塊內存變成了慢語句。**
**申請內存變成了慢語句,解決方法就是使用臨時對象池**
# 臨時對象池
如何解決這個問題,首先想到的就是對象池。Golang在`sync`里面提供了對象池`Pool`。一般大家都叫這個為對象池,而我喜歡叫它臨時對象池。因為每次垃圾回收會把池子里面不被引用的對象回收掉。
> `func (p *Pool) Get() interface{}`
需要注意的是,`Get`方法會把返回的對象從池子里面刪除。所以用完了的對象,還是得重新放回池子
~~~
package main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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個特征
1. 當每個對象的內存小于一定量的時候,不使用pool的性能秒殺使用pool;當內存處于某個量的時候,不使用pool和使用pool性能相當;當內存大于某個量的時候,使用pool的優勢就顯現出來了
2. 不使用pool,那么對象占用內存越大,性能下降越厲害;使用pool,無論對象占用內存大還是小,性能都保持不變。可以看到pool有點像飛機,雖然起步比跑車慢,但后勁十足。
即:pool適合占用內存大且并發量大的場景。當內存小并發量少的時候,使用pool適得其反
# 知識點
~~~
package main
import (
"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] = i
fmt.Printf("obj%d: %T %+v\n", i, obj, obj)
}
fmt.Println("-----------")
// 使用對象池
for i := 1; i < 3; i++ {
obj := intPool.Get().(*[]int)
(*obj)[i] = i
fmt.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真的是從池里獲得和放入池里,否則不會出現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]
~~~
1. Pool的目的是緩存已分配但未使用的項目以備后用
2. 多協程并發安全
3. 緩存在Pool里的item會沒有任何通知情況下隨時被移除,以緩解GC壓力
4. 池提供了一種方法來緩解跨多個客戶端的分配開銷。
5. 不是所有場景都適合用Pool,如果釋放鏈表是某個對象的一部分,并由這個對象維護,而這個對象只由一個客戶端使用,在這個客戶端工作完成后釋放鏈表,那么用Pool實現這個釋放鏈表是不合適的。
官方對Pool的目的描述:
Pool設計用意是在全局變量里維護的釋放鏈表,尤其是被多個 goroutine 同時訪問的全局變量。使用Pool代替自己寫的釋放鏈表,可以讓程序運行的時候,在恰當的場景下從池里重用某項值。sync.Pool一種合適的方法是,為臨時緩沖區創建一個池,多個客戶端使用這個緩沖區來共享全局資源。另一方面,如果釋放鏈表是某個對象的一部分,并由這個對象維護,而這個對象只由一個客戶端使用,在這個客戶端工作完成后釋放鏈表,那么用Pool實現這個釋放鏈表是不合適的。
**Pool的正確用法**
在Put之前重置,在Get之后重置
- 基礎
- 簡介
- 主要特征
- 變量和常量
- 編碼轉換
- 數組
- byte與rune
- big
- sort接口
- 和mysql類型對應
- 函數
- 閉包
- 工作區
- 復合類型
- 指針
- 切片
- map
- 結構體
- sync.Map
- 隨機數
- 面向對象
- 匿名組合
- 方法
- 接口
- 權限
- 類型查詢
- 異常處理
- error
- panic
- recover
- 自定義錯誤
- 字符串處理
- 正則表達式
- json
- 文件操作
- os
- 文件讀寫
- 目錄
- bufio
- ioutil
- gob
- 棧幀的內存布局
- shell
- 時間處理
- time詳情
- time使用
- new和make的區別
- container
- list
- heap
- ring
- 測試
- 單元測試
- Mock依賴
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 詳解
- plugin包
- 信號
- goto
- 協程
- 簡介
- 創建
- 協程退出
- runtime
- channel
- select
- 死鎖
- 互斥鎖
- 讀寫鎖
- 條件變量
- 嵌套
- 計算單個協程占用內存
- 執行規則
- 原子操作
- WaitGroup
- 定時器
- 對象池
- sync.once
- 網絡編程
- 分層模型
- socket
- tcp
- udp
- 服務端
- 客戶端
- 并發服務器
- Http
- 簡介
- http服務器
- http客戶端
- 爬蟲
- 平滑重啟
- context
- httptest
- 優雅中止
- web服務平滑重啟
- beego
- 安裝
- 路由器
- orm
- 單表增刪改查
- 多級表
- orm使用
- 高級查詢
- 關系查詢
- SQL查詢
- 元數據二次定義
- 控制器
- 參數解析
- 過濾器
- 數據輸出
- 表單數據驗證
- 錯誤處理
- 日志
- 模塊
- cache
- task
- 調試模塊
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密碼
- 對稱加密
- 非對稱加密
- 單向散列函數
- 消息認證
- 數字簽名
- mysql優化
- 常見錯誤
- go run的錯誤
- 新手常見錯誤
- 中級錯誤
- 高級錯誤
- 常用工具
- 協程-泄露
- go env
- gometalinter代碼檢查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高編譯
- go get
- 代理
- 其他的知識
- go內存對齊
- 細節總結
- nginx路由匹配
- 一些博客
- redis為什么快
- cpu高速緩存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密碼加密解密
- for 循環迭代變量
- 備注
- 垃圾回收
- 協程和纖程
- tar-gz
- 紅包算法
- 解決golang.org/x 下載失敗
- 逃逸分析
- docker
- 鏡像
- 容器
- 數據卷
- 網絡管理
- 網絡模式
- dockerfile
- docker-composer
- 微服務
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell調用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb