Go 语言中的 sync.Pool 学习

本博客为在学习极客时间的 go 并发编程的学习笔记,具体详细内容请看极客时间官方的专栏–go 并发编程实战课

Go 语言是自带垃圾回收机制的,具体演变过程可以看博主写的另一篇博客。所以我们不用像 C/C++ 一样,创建对象的同时,使用完需要删除/析构对象,防止因为空指针导致的内存泄漏。

但是 Garbage Collect 机制方便方便的同时,也带来的一定的性能隐患,比如 STW 机制仍然存在,我们大量在堆上创建的对象,会影响垃圾回收标记的时间。

所以在 Go 语言中,性能优化的方向一般是采用对象池的方式,把不用的对象回收起来,避免被垃圾回收掉。同样的,类似于数据库、TCP等长连接,也是保存在对象池中,可以大量减少业务的耗时!对应用程序整体性能也有一个提升。

1、sync.Pool

首先需要我们理解 sync.Pool 的两个概念

sync.Pool 数据类型是:独立访问的临时对象,本身是线程安全的,能进行并发读取其中的对象!

sync.Pool 也是不能进行复制使用的!

2、sync.Pool 的使用方法

sync.Pool 仅有三种方法:New()、Get()、Put()

1、New()

sync.Pool 中的 New() 是 func() any 类型,其中 any 在源码中用 interface{} 表示。

New 方法的使用场景为:在调用 Pool 的 Get 方法并不能从池子中获取空闲元素后,就会创建新的元素

2、Get()

调用此方法会将一个 Pool 中的一个元素取走,返回值可以为 nil 值,所以使用此方法需要对返回值进行判断。

3、Put()

此方法用于将一个元素返回给 Pool, Pool 会将此元素保存在池中,而且可以复用,但是如果值是 nil,则 Pool 会忽略此值。

3、sync.Pool 的应用场景

一般来说,很经典的场景即是 buffer 池,如 hugo 中的 bufpool,即可看到以下一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var buffers := sync.Pool {
New : func() interface {
return New(bytes.Buffer)
}
}

func GetBuffer() *bytes.Buffer {
return buffers.Get.(*bytes.Buffer)
}

func PutBuffer() (buf *bytes.Buffer) {
buf.Reset()
buffers.Put(buf)
}

然而在上述的代码,可能会导致内存泄漏的问题。

因为取出 bytes.Buffer 后,在使用时,我们通常会向此 buffer 中增加大量的 byte 数据,此时的 slice 容量可能会扩大到另一个量级的维度。

而此时我们在将其放入 Pool 中,slice 容量不改变的情况下,由于 Pool 回收的机制,这些大的 buffer 就不会被回收,而是一直留在 Pool 池中,占用着计算机的内存。

4、sync.Pool 的实现

Go 1.13 之前的版本实现的 sync.Pool 实现有两个问题:

1、每次 GC 都会回收其中创建的对象;

2、底层实现采用了:mutex,而在之前的博客 mutex 学习中,可以知道,对 mutex 锁进行并发操作,在锁竞争相当激烈的情况下,会导致性能的急剧下降。

所以 go 团队在 go 语言的1.13 版本的中,针对上述两个问题,做出了大量的优化(这也是 go 语言不建议我们在大量的并发中使用锁)。所以其中的一种优化方式就是 Pool 中不使用锁。

好用的第三方 sync.Pool 库

1、bytebufferpool

fasthttp 作者 valyala 提供的一个 buffer 池,基本功能和 sync.Pool 相同

底层使用 sync.Pool 实现的,包括会检测最大的 buffer,超过最大尺寸的 buffer,就会被丢弃。

此官方的库提供了校准(calibrate,用来动态调整创建元素的权重)的机制,可以动态地调整 Pool 的 defaultSize 和 maxSize。

2、oxtoacart/bpool

3、fatih/pool

更多详细的内容可以去大佬的课程中查看,个人觉得是 Go 并发编程课程中,讲的最好,最值得深入研究的课程!

大部分的Work Pool 都是通过 channel 来缓存任务的,因为 channel 能很好的实现并发的保护,防止数据因为并发访问所造成的 data race!

总结

如大佬所给的图:
pool2

pool 是一个通用性的概念,用于解决对象重用与预先分配的一个尝用的优化手段,当然我自己还未曾使用过,但是类似数据库连接、HTTP 的 API 请求中已经封装使用了 Pool 了。

如果在程序中 GC 耗时特别高,大量相同的类型的临时对象不断进行创建与销毁,可以考虑通过使用 sync.Pool 对其进行优化改良!


Go 语言中的 sync.Pool 学习
https://chaggle.github.io/2022/03/20/go/concurrency/Pool/
作者
chaggle
发布于
2022年3月20日
许可协议