對象池是一種在編程中用於優化資源管理的技術。它的基本思想是在應用程序啓動時預先創建一組對象,並在需要時重複使用這些對象,而不是頻繁地創建和銷燬。這種重用的機制有助於減少資源分配和回收的開銷,提高程序性能,特別在涉及大量短壽命對象的場景下效果顯著。
在Go語言中,對象池通常通過sync.Pool包或自定義數據結構實現。該機制利用Go的垃圾回收策略,通過避免不必要的對象分配來減輕垃圾回收的負擔。對象的創建、重用和釋放是對象池的核心流程,其中創建發生在對象池爲空且需要新對象時,重用則是從對象池中獲取現有對象,而釋放則是將不再需要的對象放回對象池供其他地方使用。
對象池在高併發和高性能的Go應用中具有廣泛應用。例如,在網絡編程中,可以使用對象池來維護連接池,避免頻繁地創建和關閉連接;在數據庫訪問中,對象池可以用於管理數據庫連接,減少連接的創建和銷燬開銷。這些實際應用場景充分展示了對象池在提升性能和資源利用率方面的價值。
之前在Java性能測試當中也分享了通用池化框架 Apache common-pool2
以及對應的實踐案例,今天分享一下Go語言在對象池實現上的應用。
對象池的優勢
這裏不得不簡單分享一下Go語言的垃圾回收。垃圾回收(Garbage Collection,GC)是一種自動管理內存的機制,用於檢測和釋放不再使用的內存對象,以防止內存泄漏。Go的垃圾回收機制採用了基於併發的標記-清理算法,以及部分停頓的方式來進行垃圾回收。
Go語言中,頻繁創建對象和回收對象會帶來兩個性能問題。
- 頻繁分配和銷燬對象會造成更多的內存碎片,處理這些碎片會增加額外資源開銷。
- 頻繁分配和銷燬對象會導致更頻繁的停頓時間。
- 頻繁分配和銷燬對象會帶來更多系統資源開銷。
爲了解決這個問題,處理在優化編碼質量和調整GC參數之外,對象池技術是最重要的解決方案。以上三個問題均轉化爲對象池技術的優點,
在高性能編程實踐中,對象池技術是一項不可或缺的戰略,它不僅能顯著提升系統性能,降低資源開銷,還有助於優化內存利用率。通過巧妙地重用已經存在的對象,對象池有效地規避了頻繁的對象創建和銷燬過程,減輕了系統負擔。這對於面臨資源稀缺、要求高度響應性的應用環境尤爲重要。
在高併發場景下,對象池更是發揮了巨大的作用。併發環境中,多個線程或協程可以從對象池中獲取對象,實現了資源的共享與協同,有效提高了程序的併發性能。同時,對象池還有助於避免由於頻繁的資源分配導致的內存碎片問題,優化了內存空間的使用,使系統更爲穩定。
在一個長時間運行的高性能應用中,對象池的靈活性也是其優勢之一。通過動態調整對象池的大小,可以根據實際需求進行優化,確保在不同負載下仍然能夠保持高效的性能表現。綜合而言,對象池技術的採用在高性能編程中不僅是一項優秀的實踐,更是爲了應對複雜、高併發應用場景的必備利器。
sync.Pool實現對象池
首先,Go語言自帶了 sync.Pool
實現。sync.Pool
是 Go 語言標準庫中的一個對象池實現,用於提高對象的重用性,減少對象的創建和垃圾回收的開銷。sync.Pool
在併發環境中特別有用,它能夠顯著提升程序性能。
以下是 sync.Pool
的主要特點和使用方式:
- 對象池的創建: 通過
sync.Pool
,你可以創建一個對象池,用於存儲和管理特定類型的對象。對象池中的對象在被取出後可以被重用,而不是每次都重新創建。 - Get 和 Put 操作: 使用
sync.Pool
的Get
方法可以從對象池中獲取一個對象,而Put
方法則用於將對象放回對象池。這兩個操作是併發安全的,可以被多個 goroutine 同時使用。 - 對象的生命週期:
sync.Pool
並不保證對象會一直存在,對象可能會在任意時刻被垃圾回收。因此,不能假設對象在調用Get
後一直有效,需要重新初始化。 - 適用於短生命週期對象:
sync.Pool
特別適用於管理短生命週期的對象,例如臨時對象、緩存對象等。對於長時間生存的對象,sync.Pool
的優勢可能會減弱。
下面是我用 sync.Pool
創建對象池的演示Demo:
package pool
import (
"funtester/ftool"
"log" "sync" "testing")
// PooledObject
// @Description: 對象池對象
type PooledObject struct {
Name string
Age int
Address string
}
// NewObject
//
// @Description: 創建對象
// @return *PooledObject
func NewObject() *PooledObject {
log.Println("創建對象")
return &PooledObject{
Name: "",
Age: 0,
Address: "",
}
}
// Reset
//
// @Description: 重置對象
// @receiver m 對象
func (m *PooledObject) Reset() {
m.Name = ""
m.Age = 0
m.Address = ""
log.Println("重置對象")
}
type ObjectPool struct {
ObjPool sync.Pool
Name string
}
// NewPool
//
// @Description: 創建對象池
// @param size 對象池大小
// @return *ObjectPool 對象類型
func NewPool(size int) *ObjectPool {
return &ObjectPool{
Name: "FunTester測試",
ObjPool: sync.Pool{New: func() interface{} { return NewObject() }},
}
}
// Get
//
// @Description: 獲取對象
// @receiver p 對象池
// @return *PooledObject 對象
func (p *ObjectPool) Get() *PooledObject {
return p.ObjPool.Get().(*PooledObject)
}
// Back
//
// @Description: 回收對象
// @receiver p 對象池
// @param obj 回收的對象
func (p *ObjectPool) Back(obj *PooledObject) {
obj.Reset()
p.ObjPool.Put(obj)
}
func TestPool1(t *testing.T) {
pool := NewPool(1)
get := pool.Get()
get.Name = "FunTester"
get.Age = 18
get.Address = "地球"
log.Printf("%T %s", get, ftool.ToString(get))
pool.Back(get)
get2 := pool.Get()
log.Printf("%T %s", get, ftool.ToString(get2))
}
控制檯打印:
=== RUN TestPool1
2024/01/19 23:05:17 創建對象
2024/01/19 23:05:17 *pool.PooledObject &{FunTester 18 地球}
2024/01/19 23:05:17 重置對象
2024/01/19 23:05:17 *pool.PooledObject &{ 0 }
--- PASS: TestPool1 (0.00s)
PASS
PS:這裏不建議使用併發安全類來控制對象池數量,因爲在使用過程中,對象池中的對象可能會被垃圾回收機制銷燬,會導致額外的未知問題。但是可以使用併發安全類進行借出和歸還的計數,從而實現對最大可借數量的限制,不過略微複雜,並不適用於性能測試中的場景。
chan實現對象池
我們還可以藉助 chan
來實現對象池。可以把 chan
用來存儲對象,借和還都只是從 chan
中取出和放入對象。這樣做的好處如下幾點:
- 併發安全。由於
chan
操作是原子性的,所以整個的借還過程都是併發安全的。 - 數量可控。可以通過設置
chan
的容量控制對象總量。 - 阻塞處理。當無足夠對象或者過多對象時,可以阻塞以便進行邏輯處理。
- 可讀性好。使用
chan
實現對象池,代碼清晰易讀,便於維護。
下面是我的實現Demo:
package pool
import (
"log"
"reflect" "testing")
type ObjectPool2 struct {
objects chan *PooledObject
Name string
}
// NewPool
//
// @Description: 創建對象池
// @param size 對象池大小
// @return *ObjectPool 對象類型
func NewPool2(size int) *ObjectPool2 {
return &ObjectPool2{
objects: make(chan *PooledObject, size),
Name: "FunTester測試",
}
}
// Get
//
// @Description: 獲取對象
// @receiver p 對象池
// @return *PooledObject 對象
func (p *ObjectPool2) Get2() *PooledObject {
select {
case obj := <-p.objects:
return obj
default:
log.Println("額外創建對象")
return NewObject()
}
}
// Back
//
// @Description: 回收對象
// @receiver p 對象池
// @param obj 回收的對象
func (p *ObjectPool2) Back(obj *PooledObject) {
obj.Reset()
select {
case p.objects <- obj:
default:
obj = nil
log.Println("丟棄對象")
}
}
func TestPool2(t *testing.T) {
pool := NewPool2(1)
get := pool.Get2()
object := pool.Get2()
log.Printf("%T", get)
log.Println(reflect.TypeOf(get))
pool.Back(get)
pool.Back(object)
}
控制檯輸出:
=== RUN TestPool2
2024/01/19 23:19:42 額外創建對象
2024/01/19 23:19:42 創建對象
2024/01/19 23:19:42 額外創建對象
2024/01/19 23:19:42 創建對象
2024/01/19 23:19:42 *pool.PooledObject
2024/01/19 23:19:42 *pool.PooledObject
2024/01/19 23:19:42 重置對象
2024/01/19 23:19:42 重置對象
2024/01/19 23:19:42 丟棄對象
--- PASS: TestPool2 (0.00s)
PASS
雖然chan
實現對象池在某些場景下具有優勢,但在其他情況下可能不是最佳選擇。在一些性能要求較高的場景中,使用更爲專業的對象池庫或者手動管理對象池的方式可能更爲靈活和高效。
第三方庫
在Go語言中,有一些第三方庫專門用於實現對象池,它們提供了更復雜、靈活、高效的對象池管理機制。以下是一些常用的第三方庫,用於實現對象池:
github.com/fatih/pool
:- GitHub 地址: fatih/pool
- 該庫提供了一個通用的對象池實現,支持對任意對象的池化。它允許你自定義對象的創建、銷燬和驗證邏輯,非常靈活。
github.com/panjf2000/ants/v2
:- GitHub 地址: panjf2000/ants
- 該庫是一個高性能的 goroutine 池,適用於需要併發執行任務的場景。雖然主要關注 goroutine 池,但也可以用作通用的對象池。
github.com/jolestar/go-commons-pool
:- GitHub 地址: jolestar/go-commons-pool
- 該庫是一個通用的對象池實現,支持池化各種類型的對象。它提供了豐富的配置選項,允許你自定義對象創建、銷燬和驗證的邏輯。
github.com/avast/retry-go
:- GitHub 地址: avast/retry-go
- 該庫提供了一個靈活的對象池實現,支持對獲取和釋放對象的重試策略。適用於需要在獲取對象時進行重試的情況。
這些庫提供了比標準庫的 sync.Pool
和 chan
實現 更爲複雜且靈活,可以根據具體需求進行選擇。後面有機會我會選擇其中一兩種學習實踐,然後分享。