Pool是對象池模式的并發安全實現。關于對象池模式的完整解釋最好留給有關設計模式的文獻(如 Head First Design Patterns)。不過既然Pool出現在了標準庫中,就讓我們簡要討論下為什么你可能有興趣使用它。
在較高的層次上,池模式是一種創建和提供固定數量可用對象的方式。它通常用于約束創建資源昂貴的事物(例如數據庫連接)。Go的sync.Pool可以被多個例程安全地使用。
Pool的主要接口是它的Get方法。 被調用時,Get將首先檢查池中是否有可用實例返回給調用者,如果沒有,則創建一個新成員變量。使用完成后,調用者調用Put將正在使用的實例放回池中供其他進程使用。 這里有一個簡單的例子來演示:
```
myPool := &sync.Pool{
New: func() interface{} {
fmt.Println("Creating new instance.")
return struct{}{}
},
}
myPool.Get() //1
instance := myPool.Get() //1
myPool.Put(instance) //2
myPool.Get() //3
```
1. 這里我們調用Get方法,將調用在池中定義的New函數,因為實例尚未實例化。
2. 在這里,我們將先前檢索到的實例放回池中。 這時實例的可用數量為1個。
3. 執行此調用時,我們將重新使用先前分配的實例。New函數不會被調用。
我們可以看到,這回調用2次New函數:
```
Creating new instance.
Creating new instance.
```
那么為什么要使用一個池,而不是實例化對象呢? Go有一個垃圾收集器,所以實例化的對象將被自動清理。 重點是什么? 考慮這個例子:
```
var numCalcsCreated int
calcPool := &sync.Pool{
New: func() interface{} {
numCalcsCreated += 1
mem := make([]byte, 1024)
return &mem // 1
},
}
// 將池擴充到4KB
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
const numWorkers = 1024 * 1024
var wg sync.WaitGroup
wg.Add(numWorkers)
for i := numWorkers; i > 0; i-- {
go func() {
defer wg.Done()
mem := calcPool.Get().(*[]byte) // 2
defer calcPool.Put(mem)
}()
}
// 假設內存中執行了一些快速的操作
wg.Wait()
fmt.Printf("%d calculators were created.", numCalcsCreated)
```
1. 注意,我們存儲了字節切片的指針。
2. 這里我們斷言此類型是一個指向字節切片的指針。
這會輸出:
```
8 calculators were created.
```
如果我沒有使用sync.Pool運行此示例,那么結果將是非確定性的,但在最壞的情況下,我可能需要分配千兆字節的內存。但正如你從輸出中看到的那樣,我只分配了4 KB 。
Pool有用的另一種常見情況是預熱分配對象的緩存,用于必須盡快運行的操作。 在這種情況下,我們不是通過限制創建對象的數量來保護主機的內存,而是通過預先加載獲取對另一個對象的引用來減少消費者的時間消耗。在編寫高吞吐量網絡服務器時,這是非常常見的。我們來看看這種情況。
首先,我們來創建一個模擬創建服務連接的函數。 我們會讓這個連接花費很長時間:
```
func connectToService() interface{} {
time.Sleep(1 * time.Second)
return struct{}{}
}
```
接下來,讓我們看看如果對于每個請求都開啟一個新的服務連接,網絡服務的性能如何。我們將編寫一個網絡處理程序,為了簡化基準測試,我們一次只允許一個連接:
```
func startNetworkDaemon() *sync.WaitGroup {
var wg sync.WaitGroup
wg.Add(1)
go func() {
server, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatalf("cannot listen: %v", err)
}
defer server.Close()
wg.Done()
for {
conn, err := server.Accept()
if err != nil {
log.Printf("cannot accept connection: %v", err)
continue
}
connectToService()
fmt.Fprintln(conn, "")
conn.Close()
}
}()
return &wg
}
```
現在我們可以著手進行基準測試了:
```
func init() {
daemonStarted := startNetworkDaemon()
daemonStarted.Wait()
}
func BenchmarkNetworkRequest(b *testing.B) {
for i := 0; i < b.N; i++ {
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
b.Fatalf("cannot dial host: %v", err)
}
if _, err := ioutil.ReadAll(conn); err != nil {
b.Fatalf("cannot read: %v", err)
}
conn.Close()
}
}
```
在命令行執行:
```
cd src/gos-concurrency-building-blocks/the-sync-package/pool/ && \
go test -benchtime=10s -bench=.
```
這會輸出:
| BenchmarkNetworkRequest-8 | 10 | 1000385643 ns/op |
| --- | --- | --- |
| PASS ok | command-line-arguments | 11.008s |
就性能而言這看起來挺合理。讓我們加上sync.Pool再試試:
```
func warmServiceConnCache() *sync.Pool {
p := &sync.Pool{
New: connectToService,
}
for i := 0; i < 10; i++ {
p.Put(p.New())
}
return p
}
func startNetworkDaemon() *sync.WaitGroup {
var wg sync.WaitGroup
wg.Add(1)
go func() {
connPool := warmServiceConnCache()
server, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatalf("cannot listen: %v", err)
}
defer server.Close()
wg.Done()
for {
conn, err := server.Accept()
if err != nil {
log.Printf("cannot accept connection: %v", err)
continue
}
svcConn := connPool.Get()
fmt.Fprintln(conn, "")
connPool.Put(svcConn)
conn.Close()
}
}()
return &wg
}
```
同樣執行基準測試,
```
cd src/gos-concurrency-building-blocks/the-sync-package/pool && \
go test -benchtime=10s -bench=.
```
我們可以看到:
| BenchmarkNetworkRequest-8 | 5000 | 2904307 ns/op |
| --- | --- | --- |
|PASS ok | command-line-arguments | 32.647s |
整整快了三個數量級!你可以看到我們是如何利用這種模式如何大大縮短響應時間的。
正如這個例子所展現的,池模式非常適合于這種需要并發進程,或者構建這些對象可能會對內存產生負面影響的應用程序。
但是,在確定是否應該使用池時有一點需要注意:如果使用池子里東西在內存上不是大致均勻的,則會花更多時間將從池中檢索,這比首先實例化它要耗費更多的資源。例如,你的程序需要隨機和可變長度的切片,在這種情況下Pool不會為你提供太多的幫助。
因此,在使用Pool時,請記住以下幾點:
* 實例化sync.Pool時,給它一個新元素,該元素應該是線程安全的。
* 當你從Get獲得一個實例時,不要假設你接收到的對象狀態。
* 當你從池中取得實例時,請務必不要忘記調用Put。否則池的優越性就體現不出來了。這通常用defer來執行延遲操作。
* 池中的元素必須大致上是均勻的。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度