基本思想
关键思想是对象的复用,避免重复创建、销毁。将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力。
实现原理
sync.Pool
的最底层使用切片加链表来实现双端队列,并将缓存的对象存储在切片中。
type Pool struct {
noCopy noCopy
// 每个 P 的本地队列,实际类型为 [P]poolLocal
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
// [P]poolLocal的大小
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数
New func() interface{}
}
注意事项
- Pool 不可以指定⼤⼩,⼤⼩只受制于 GC 临界值。
- Pool 里对象的生命周期受 GC 影响,不适合于做连接池,因为连接池需要自己管理对象的生命周期。
- 不要对 Get 得到的对象有任何假设,更好的做法是归还对象时,将对象“清空”。
什么时候用
对象池适合在以下场景中使用:
- 对象的创建开销较大,如大字符串、大结构体、大切片等。使用对象池可以显著减少对象创建的开销。
- 对象的创建频率高,但每个对象的生命周期较短。这种情况下,频繁地创建和销毁对象会导致 GC 压力增大,使用对象池可以缓解这种压力。
- 并发量大,多个 goroutine 都需要创建同一类对象。对象池可以减少锁的争用,提高并发性能。
- 对象的初始化状态可以抽象为一个函数(
sync.Pool
的New
函数)。这样可以方便地在对象被获取时重置其状态。
demo
package main
import (
"bytes"
"fmt"
"sync"
"time"
)
func withoutPool() {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
var buffer bytes.Buffer
for j := 0; j < 100; j++ {
buffer.WriteString("Hello, world!")
}
_ = buffer.String()
}()
}
wg.Wait()
elapsed := time.Since(start)
fmt.Printf("Without pool: %s\n", elapsed)
}
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func withPool() {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
buffer := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buffer)
buffer.Reset()
for j := 0; j < 100; j++ {
buffer.WriteString("Hello, world!")
}
_ = buffer.String()
}()
}
wg.Wait()
elapsed := time.Since(start)
fmt.Printf("With pool: %s\n", elapsed)
}
func main() {
withoutPool()
withPool()
}
输出结果
Without pool: 1.6958312s
With pool: 433.6141ms
可以看到,性能差别还是很明显的
什么时候不适合用
在以下场景中,使用对象池可能并不合适:
- 对象的生命周期需要精确控制。
sync.Pool
中的对象随时可能被 GC 回收,无法保证对象的生命周期,因此不适合用于如数据库连接池这样需要长期持有的对象。 - 对象的创建和销毁开销并不大,或者对象的创建频率较低。在这种情况下,使用对象池的收益可能无法抵消其引入的复杂性。
- 对象的状态需要长期保持。从
sync.Pool
获取的对象状态是不可预知的,如果对象的状态需要在多次使用中保持,那么使用对象池可能会引入 bug。 - 代码的可读性和可维护性要求高于性能。使用
sync.Pool
会增加代码的复杂性,降低其可读性,在某些场景下这可能并不是一个好的权衡。
总之,sync.Pool
是一个强大的工具,但并非银弹。在决定是否使用对象池时,需要仔细权衡其收益与成本,根据具体的场景和需求进行选择。