localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
New func() any
}
func (p *Pool) Put(x any) {}
func (p *Pool) Get() any {}
Get
方法: 从sync.Pool
中取出缓存对象
Put
方法: 将缓存对象放入到sync.Pool
当中
New
函数: 在创建sync.Pool
时,需要传入一个New
函数,当Get
方法获取不到对象时,此时将会调用New
函数创建新的对象返回。
3.1.2 使用方式
当使用sync.Pool
时,通常需要以下几个步骤:
- 首先使用
sync.Pool
定义一个对象缓冲池
- 在需要使用到对象时,从缓冲池中取出
- 当使用完之后,重新将对象放回缓冲池中
下面是一个简单的代码的示例,展示了使用sync.Pool
大概的代码结构:
type struct data{
// 定义一些属性
}
//1. 创建一个data对象的缓存池
var dataPool = sync.Pool{New: func() interface{} {
return &data{}
}}
func Operation_A(){
// 2. 需要用到data对象的地方,从缓存池中取出
d := dataPool.Get().(*data)
// 执行后续操作
// 3. 将对象重新放入缓存池中
dataPool.Put(d)
}
3.2 使用例子
下面我们使用sync.Pool
来对IntToStringMap
进行改造,实现对bytes.Buffer
对象的重用,同时也能够自动根据系统当前的状况,自动调整缓冲池中对象的数量。
// 1. 定义一个bytes.Buffer的对象缓冲池
var buffers sync.Pool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func IntToStringMap(m map[string]int) (string, error) {
// 2. 在需要的时候,从缓冲池中取出一个bytes.Buffer对象
buf := buffers.Get().(*bytes.Buffer)
buf.Reset()
// 3. 用完之后,将其重新放入缓冲池中
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
}
上面我们使用sync.Pool
实现了一个bytes.Buffer
的缓冲池,在 IntToStringMap
函数中,我们从 buffers
中获取一个 bytes.Buffer
对象,并在函数结束时将其放回池中,避免了频繁创建和销毁 bytes.Buffer
对象的开销。
同时,由于sync.Pool
在IntToStringMap
调用不频繁的情况下,能够自动回收sync.Pool
中的bytes.Buffer
对象,无需用户操心,也能减小内存的压力。而且其底层实现也有考虑到多核cpu并发执行,每一个processor都会有其对应的本地缓存,在一定程度也减少了多线程加锁的开销。
从上面可以看出,sync.Pool
使用起来非常简单,但是其还是存在一些注意事项,如果使用不当的话,还是有可能会导致内存泄漏等问题的,下面就来介绍sync.Pool
使用时的注意事项。
4.使用注意事项
4.1 需要注意放入对象的大小
如果不注意放入sync.Pool
缓冲池中对象的大小,可能出现sync.Pool
中只存在几个对象,却占据了大量的内存,导致内存泄漏。
这里对于有固定大小的对象,并不需要太过注意放入sync.Pool
中对象的大小,这种场景出现内存泄漏的可能性小之又小。但是,如果放入sync.Pool
中的对象存在自动扩容的机制,如果不注意放入sync.Pool
中对象的大小,此时将很有可能导致内存泄漏。下面来看一个例子:
func Sprintf(format string, a ...any) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
Sprintf
方法根据传入的format和对应的参数,完成组装,返回对应的字符串结果。按照普通的思路,此时只需要申请一个byte
数组,然后根据一定规则,将format
和参数
的内容放入byte
数组中,最终将byte
数组转换为字符串返回即可。
按照上面这个思路我们发现,其实每次使用到的byte
数组是可复用的,并不需要重复构建。
实际上Sprintf
方法的实现也是如此,byte
数组其实并非每次创建一个新的,而是会对其进行复用。其实现了一个pp
结构体,format
和参数
按照一定规则组装成字符串的职责,交付给pp
结构体,同时byte
数组作为pp
结构体的成员变量。
然后将pp
的实例放入sync.Pool
当中,实现pp
重复使用目的,从而简介避免了重复创建byte
数组导致频繁的GC,同时也提升了性能。下面是newPrinter
方法的逻辑,获取pp
结构体,都是从sync.Pool
中获取:
var ppFree = sync.Pool{
New: func() any { return new(pp) },
}
// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
// 从ppFree中获取pp
p := ppFree.Get().(*pp)
// 执行一些初始化逻辑
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
下面回到上面的byte
数组,此时其作为pp
结构体的一个成员变量,用于字符串格式化的中间结果,定义如下:
// Use simple []byte instead of bytes.Buffer to avoid large dependency.
type buffer []byte
type pp struct {
buf buffer
// 省略掉其他不相关的字段
}
这里看起来似乎没啥问题,但是其实是有可能存在内存浪费甚至内存泄漏的问题。假如此时存在一个非常长的字符串需要格式化,此时调用Sprintf
来实现格式化,此时pp
结构体中的buffer
也同样需要不断扩容,直到能够存储整个字符串的长度为止,此时pp
结构体中的buffer
将会占据比较大的内存。
当Sprintf
方法完成之后,重新将pp
结构体放入sync.Pool
当中,此时pp
结构体中的