sync.Pool 是 Golang 内置的对象池技术,可用于缓存临时对象,以缓解因频繁建立临时对象带来的性能损耗以及对 GC 带来的压力。
在许多知名的开源库中都可以看到 sync.Pool 的大量使用。例如,HTTP 框架 Gin 用 sync.Pool 来复用每个请求都会创建的 gin.Context 对象。 在 grpc-Go、kubernetes 等也都可以看到对 sync.Pool 的身影。
但需要注意的是,sync.Pool 缓存的对象随时可能被无通知的清除,因此不能将 sync.Pool 用于存储持久对象的场景。
sync.Pool 作为 goroutine 内置的官方库,其设计非常精妙。sync.Pool 不仅是并发安全的,而且实现了 lock free,里面有许多值得学习的知识点。
本文将基于 go-1.16 的源码 对 sync.Pool 的底层实现一探究竟。
基本用法
在正式讲 sync.Pool 底层之前,我们先看下 sync.Pool 的基本用法。其示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
type Test struct {
A int
}
func main() {
pool := sync.Pool{
New: func() interface{} {
return &Test{
A: 1,
}
},
}
testObject := pool.Get().(*Test)
println(testObject.A) // print 1
pool.Put(testObject)
}
|
sync.Pool 在初始化的时候,需要用户提供一个对象的构造函数 New。用户使用 Get 来从对象池中获取对象,使用 Put 将对象归还给对象池。整个用法还是比较简单的。
接下来,让我们详细看下 sync.Pool 是如何实现的。
sync.Pool 的底层实现
在讲 sync.Pool 之前,我们先聊下 Golang 的 GMP 调度。在 GMP 调度模型中,M 代表了系统线程,而同一时间一个 M 上只能同时运行一个 P。那么也就意味着,从线程维度来看,在 P 上的逻辑都是单线程执行的。
sync.Pool 就是充分利用了 GMP 这一特点。对于同一个 sync.Pool ,它在每个 P 上都分配了一个本地对象池 poolLocal。如下图所示。

sync.Pool 的 代码定义如下 sync/pool.go#L44:
1 2 3 4 5 6 7 8 9 10 11 |
type Pool struct {
noCopy noCopy
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 func() interface{}
}
|
其中,我们需要着重关注以下三个字段:
local是个数组,长度为 P 的个数。其元素类型是poolLocal。这里面存储着各个 P 对应的本地对象池。可以近似的看做[P]poolLocal。localSize。代表 local 数组的长度。因为 P 可以在运行时通过调用 runtime.GOMAXPROCS 进行修改, 因此我们还是得通过localSize来对应local数组的长度。New就是用户提供的创建对象的函数。这个选项也不是必需。当不填的时候,Get 有可能返回 nil。 <

最低0.47元/天 解锁文章
1201

被折叠的 条评论
为什么被折叠?



