Go并发:使用sync.Pool来性能优化
简介
在Go提供如何实现对象的缓存池功能?常用一种实现方式是:sync.Pool, 其旨在缓存已分配但未使用的项目以供以后重用,从而减轻垃圾收集器(GC)的压力。
快速使用
sync.Pool的结构也比较简单,常用的方法有Get、Put
type Pool struct {
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
func (p *Pool) Get() any
func (p *Pool) Put(x any)
接着,通过一个简单的例子,来看看是如何使用的
package main
import (
"fmt"
"sync"
)
type Object struct {
ID int
// ...
}
func main() {
// 1.创建一个sync.Pool对象
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Creating a new object")
return &Object{}
},
}
// 2.pool.Get()方法从池中获取一个对象。如果池中有可用的对象,Get()方法将返回其中一个;否则,它将返回一个新创建的对象
obj := pool.Get().(*Object)
// 3.操作对象
obj.ID = 1
// 4.调用pool.Put()方法将对象放回池中
pool.Put(obj)
objBar := pool.Get().(*Object)
fmt.Println("Object ID:", objBar.ID)
}
实践应用
在之前的文章中有提到的享元模式设计模式:flyweight(享元)的在棋牌游戏的应用的案例。今天我们使用sync.Pool对该方案进行优化。
观察在棋牌游戏的代码,虽然解决了每次都要New一个对象的问题,但还存在几个优化点:
不能只能缓存特定的棋牌室类型对象;
并发安全问题
原来是通过Factory工厂+Map实现享元模式,截取其中部分代码如下
package design_mode
import "fmt"
var chessPieceUnit = map[int]*ChessPiece{
1: {
Name: "車",
Color: "紅",
PositionX: 1,
PositionY: 11,
},
2: {
Name: "馬",
Color: "黑",
PositionX: 2,
PositionY: 2,
},
// 其他棋子
}
func NewChessPieceUnitFactory() *ChessBoard {
board := &ChessBoard{Cards: map[int]*ChessPiece{}}
for id := range chessPieceUnit {
board.Cards[id] = chessPieceUnit[id]
}
return board
}
1.重构Factory
接着,我们同sync.Pool修改一下Factory的实现:
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Creating a new object")
return NewChessBoard()
},
}
game1 := pool.Get().(*ChessBoard)
game2 := pool.Get().(*ChessBoard)
fmt.Println(game1)
fmt.Println(game2)
fmt.Println(game1.Cards[0] == game2.Cards[0])
2. 并发安全问题
2.1 修改模型
为了方便观察,给每个房间(棋牌室)增加一个创建时间
type ChessBoard struct {
Cards map[int]*ChessPiece
Time time.Time
}
2.2 并发测试
启动多个goroutine进行测试
func main() {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Creating a new object")
return NewChessBoard()
},
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
obj := pool.Get().(*ChessBoard)
obj.Time = time.Now()
pool.Put(obj)
fmt.Printf("Object ID: %v\n", obj.Time)
}(i)
}
wg.Wait()
}
输出如下:
Creating a new object
Creating a new object
Object ID: 2023-10-22 15:41:50.309343 +0800 CST m=+0.003511901
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
Object ID: 2023-10-22 15:41:50.3117423 +0800 CST m=+0.005911201
可见,在多个goroutine的并发情况下,是安全,另外可以观察到,sync.Pool没有一直【Creating a new object】去New很多棋牌室。
小结
sync.Pool是Go语言标准库中的一个类型,它提供了对象的缓存池功能。它的主要用途是存储那些可以被复用的临时对象,以便在需要时快速获取,而不是每次都进行新的对象分配。且多个 goroutine 同时使用 Pool 是安全的。
本文简述了sync.Pool的基础使用,以及了如何使用其对实践棋牌室游戏的案例进行优化过程。
参考
官方doc
设计模式:flyweight(享元