1. 简介
本文将介绍 Go 语言中的 sync.Pool
并发原语,包括sync.Pool
的基本使用方法、使用注意事项等的内容。能够更好得使用sync.Pool
来减少对象的重复创建,最大限度实现对象的重复使用,减少程序GC的压力,以及提升程序的性能。
2. 问题引入
2.1 问题描述
这里我们实现一个简单的JSON序列化器,能够实现将一个map[string]int
序列化为一个JSON字符串,实现如下:
func IntToStringMap(m map[string]int) (string, error) {
// 定义一个bytes.Buffer,用于缓存数据
var buf bytes.Buffer
buf.Write([]byte("{"))
for k, v := range m {
buf.WriteString(fmt.Sprintf(`"%s":%d,`, k, v))
}
if len(m) > 0 {
buf.Truncate(buf.Len() - 1) // 去掉最后一个逗号
}
buf.Write([]byte("}"))
return buf.String(), nil
}
这里使用bytes.Buffer
来缓存数据,然后按照key:value
的形式,将数据生成一个字符串,然后返回,实现是比较简单的。
每次调用IntToStringMap
方法时,都会创建一个bytes.Buffer
来缓存中间结果,而bytes.Buffer
其实是可以被重用的,因为序列化规则和其并没有太大的关系,其只是作为一个缓存区来使用而已。
但是当前的实现为每次调用IntToStringMap
时,都会创建一个bytes.Buffer
,如果在一个应用中,请求并发量非常高时,频繁创建和销毁bytes.Buffer
将会带来较大的性能开销,会导致对象的频繁分配和垃圾回收,增加了内存使用量和垃圾回收的压力。
那有什么方法能够让bytes.Buffer
能够最大程度得被重复利用呢,避免重复的创建和回收呢?
2.2 解决方案
其实我们可以发现,为了让bytes.Buffer
能够被重复利用,避免重复的创建和回收,我们此时只需要将bytes.Buffer
缓存起来,在需要时,将其从缓存中取出;当用完后,便又将其放回到缓存池当中。这样子,便不需要每次调用IntToStringMap
方法时,就创建一个bytes.Buffer
。
这里我们可以自己实现一个缓存池,当需要对象时,可以从缓存池中获取,当不需要对象时,可以将对象放回缓存池中。IntToStringMap
方法需要bytes.Buffer
时,便从该缓存池中取,当用完后,便重新放回缓存池中,等待下一次的获取。下面是一个使用切片实现的一个bytes.Buffer
缓存池。
type BytesBufferPool struct {
mu sync.Mutex
pool []*bytes.Buffer
}
func (p *BytesBufferPool) Get() *bytes.Buffer {
p.mu.Lock()
defer p.mu.Unlock()
n := len(p.pool)
if n == 0 {
// 当缓存池中没有对象时,创建一个bytes.Buffer
return &bytes.Buffer{}
}
// 有对象时,取出切片最后一个元素返回
v := p.pool[n-1]
p.pool[n-1] = nil
p.pool = p.pool[:n-1]
return v
}
func (p *BytesBufferPool) Put(buffer *bytes.Buffer) {
if buffer == nil {
return
}
// 将bytes.Buffer放入到切片当中
p.mu.Lock()
defer p.mu.Unlock()
obj.Reset()
p.pool = append(p.pool, buffer)
}
上面BytesBufferPool
实现了一个bytes.Buffer
的缓存池,其中Get
方法用于从缓存池中取对象,如果没有对象,就创建一个新的对象返回;Put
方法用于将对象重新放入BytesBufferPool
当中,下面使用BytesBufferPool
来优化IntToStringMap
。
// 首先定义一个BytesBufferPool
var buffers BytesBufferPool
func IntToStringMap(m map[string]int) (string, error) {
// bytes.Buffer不再自己创建,而是从BytesBufferPool中取出
buf := buffers.Get()
// 函数结束后,将bytes.Buffer重新放回缓存池当中
defer buffers.Put(buf)
buf.Write([]byte("{"))
for k, v := range m {
buf.WriteString(fmt.Sprintf(`"%s":%d,`, k, v))
}
if len(m) > 0 {
buf.Truncate(buf.Len() - 1) // 去掉最后一个逗号
}
buf.Write([]byte("}"))
return buf.String(), nil
}
到这里我们通过自己实现了一个缓存池,成功对InitToStringMap
函数进行了优化,减少了bytes.Buffer
对象频繁的创建和回收,在一定程度上提高了对象的频繁创建和回收。
但是,BytesBufferPool
这个缓存池的实现,其实存在几点问题,其一,只能用于缓存bytes.Buffer
对象;其二,不能根据系统的实际情况,动态调整对象池中缓存对象的数量。假如某段时间并发量较高,bytes.Buffer
对象被大量创建,用完后,重新放回BytesBufferPool
之后,将永远不会被回收,这有可能导致内存浪费,严重一点,也会导致内存泄漏。
既然自定义缓存池存在这些问题,那我们不禁要问,Go语言标准库中有没有提供了更方便的方式,来帮助我们缓存对象呢?
别说,还真有,Go标准库提供了sync.Pool
,可以用来缓存那些需要频繁创建和销毁的对象,而且它支持缓存任何类型的对象,同时sync.Pool
是可以根据系统的实际情况来调整缓存池中对象的数量,如果一个对象长时间未被使用,此时将会被回收掉。
相对于自己实现的缓冲池,sync.Pool
的性能更高,充分利用多核cpu的能力,同时也能够根据系统当前使用对象的负载,来动态调整缓冲池中对象的数量,而且使用起来也比较简单,可以说是实现无状态对象缓存池的不二之选。
下面我们来看看sync.Pool
的基本使用方式,然后将其应用到IntToStringMap
方法的实现当中。
3. 基本使用
3.1 使用方式
3.1.1 sync.Pool的基本定义
sync.Pool
的定义如下: 提供了Get
,Put
两个方法:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal