概述
sync.Pool 是 Go 语言标准库中的一个并发安全的对象池,可以用来缓存那些需要重复创建和销毁的对象,从而避免频繁地进行内存分配和回收,降低内存和 GC 压力。
需要注意的是: 任何存储在对象池中的元素可能会被随时删除,如果元素是一个资源类的引用,并且该资源仅在对象池中被引用 (没有其他地方引用了),那么当该元素被对象池删除时,其指向的资源同时也会被释放。
内部实现
sync.Pool 的使用方法相信读者已经熟练掌握,本文主要来探究一下底层源代码实现,文件路径为 $GOROOT/src/sync/pool.go
,笔者的 Go 版本为 go1.19 linux/amd64
。
💡 sync.Pool 的源代码中细节非常之多,为了阅读体验和效率,笔者几乎没有删减代码,而且也基本对每行代码都做了对应的注解和上下文联系,这是本文的特色,请读者留意。
数据结构
全局变量
var (
// 锁
allPoolsMu Mutex
// 全局的所有缓存池
allPools []*Pool
// victim cache 缓存池
oldPools []*Pool
)
数据结构图
这里假设 runtime.GOMAXPROCS() = 4
, 处理器 P 的数量为 4 个,读者在阅读下面的源代码探究过程时,可以对照着结构图进行分析。
缓存池对象
sync.Pool 包的核心对象,所有的操作都是基于该对象进行的。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
这里引用下维基百科关于 victim cache
的描述:
简单通俗地来说,就是已经失效的缓存先不清除,保留一段时间,如果保留时间内该缓存又被用到了,就重新启用,如果保留时间内一直没有被用到,就清除。
poolLocal 对象
每个处理器 P
都有一个 poolLocal
对象,Get
和 Put
方法会优先操作当前处理器的对象池。
type poolLocal struct {
poolLocalInternal
// CPU Cache 是距离 CPU 最近的 Cache,如果能充分利用,会极大提升程序性能
// 防止伪共享,凑齐 128 bytes 的整数倍 (这个小技巧非常值得学习)
// 什么是CPU 伪共享?
// CPU CacheLine 通常是以 64 byte 或 128 byte 为单位
// 在缓存池场景中,各个 P 的 poolLocal 以数组形式存储在一起
// 假设 CPU CacheLine 为 128 byte,而 poolLocal 不足 128 byte 时
// CacheLine 将会带上其他 P 的 poolLocal 的内存数据,以凑齐一个整块的 CacheLine
// 如果这时两个相邻的 P 同时在两个不同的 CPU 核上运行,将会同时去覆盖刷新 CacheLine
// 造成 CacheLine 的反复失效,那 CPU Cache 就失去了作用
// 例如 两个相邻但是不同的处理器 P (PA, PB) 被分配在同一个 CacheLine
// 此时 PA 要修改, PB 也要修改 (两者去竞争 同一个 CacheLine)
// 当 PA 被修改时,缓存系统强制 PB 所在 CPU 核的 CacheLine 失效
// 当 PB 被修改时,缓存系统强制 PA 所在 CPU 核的 CacheLine 失效
// 最终导致 PA 和 PB 所在 CPU 核的 CacheLine 失效,降低性能
// 如何避免 CPU 伪共享?
// 将需要独立访问的变量放在不同的 CacheLine 中
// 保证和 CacheLine 内存对齐
// Linux 查看 CacheLine 单位大小
// $ cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
poolLocalInternal 对象
poolLocalInternal 对象表示每个处理器 P 的本地对象池。
type poolLocalInternal struct {
// 私有变量,只能由当前处理器操作
private any
// 共享变量,当前处理器可以执行 pushHead/popHead 操作,其他处理器只能执行 popTail 操作
shared poolChain
}
Go 1.13 版本开始,shared
字段的数据结构修改为 单个生产者/多个消费者
双端无锁环形队列,当前处理器 P 可以执行 pushHead/popHead
操作, 其他处理器 P 只能执行 popTail
操作。
单个生产者:当前处理器 P
上面运行的 goroutine
执行 Put
方法时,将对象放入队列,并且只能放在队列头部,但是其他处理器 P 上运行的 goroutine
不能放入。 由于每个处理器 P 在任意时刻只有一个 goroutine
运行,所以无需加锁。
多个消费者分两种角色:
在当前处理器 P 上运行的 goroutine
,执行Get
方法时,从队列头部取对象,由于每个处理器 P 在任意时刻只有一个goroutine
运行,所以无需加锁在其他处理器 P 上运行的 goroutine
,执行Get
方法时,如果该处理器 P 没有缓存对象,就到别的处理器 P 的队列上窃取。 此时窃取者goroutine
只能从队列尾部取对象,因为同时可能有多个窃取者goroutine
窃取同一个处理器 P 的队列, 所以用CAS
来实现无锁队列功能
按照这种设计,poolDequeue.pushHead
和 poolDequeue.popTail
存在竞争 (可能同时有多个 goroutine
同时操作), 而 poolDequeue.pushHead
和 poolDequeue.popHead
不存在竞争 (只能有一个 goroutine
操作)。
poolDequeue.pushHead: 将对象添加到队列头部 poolDequeue.popHead : 从队列头部获取对象 poolDequeue.popTail : 从队列尾部获取对象
poolChain 对象
poolChain 对象表示 poolDequeue 数据类型的双端环形队列链表,每个节点表示的队列长度是后驱节点队列长度的两倍, 如果当前所有的节点队列满了,就创建一个新的队列 (长度是当前头节点队列长度的 2 倍),然后挂载到头节点。
// 队列节点示意图
// --------------------------------------------------------------------------
// | 节点 1, size: 64 | 节点 2, size: 32 | 节点 3, size: 16 | 节点 4, size: 8 |
// --------------------------------------------------------------------------
type poolChain struct {
// head 表示头节点队列,只能由生产者操作,不存在竞争
head *poolChainElt
// tail 表示尾节点队列,由多个消费者操作,存在竞争
tail *poolChainElt
}
type poolChainElt struct {
poolDequeue
// next 由生产者原子性写入,由消费者原子性读取
// 值只会从 nil 转换为非 nil
// prev 由消费者原子性写入,由生产者原子性读取
// 值只会从非 nil 转换为 nil
next, prev *poolChainElt
}
为什么 poolChain 的数据结构是链表 + ring buffer (环形队列) 呢?
因为使用 ring buffer
数据结构的优点非常适用于 sync.Pool
对象池的使用场景。
预先分配好内存并且分配的元素内存可复用,避免了数据迁移 作为底层数据结构的数组是连续内存结构,非常利于 CPU Cache
, 在访问poolDequeue
队列中的某个元素时,其附近的元素可能被加载到同一个Cache Line
中,访问速度更快更高效的出队和入队操作,因为环形队列是首尾相连的,避免了普通队列中队首和队尾频繁变动的问题
poolDequeue 对象
poolDequeue 对象是一个由 单个生产者/多个消费者
模式组成的固定大小的无锁队列。单个生产者可以从队列头部执行 push
和 pop
操作, 多个消费者只能从队列尾部执行 pop
操作。
type poolDequeue struct {
// 经典的字段合并使用方法
// 高 32 位 是 head, 指向下一个存放对象的索引
// 低 32 位 是 tail, 指向队列中最早 (下一个读取) 的对象索引
// 索引区间 tail <= i < head, 表示消费者可以操作的索引区域
// 消费者可以在该区间不断获取对象,直至获取到的对象为 nil
headTail uint64
// vals 表示队列元素容器,大小必须为 2 的 N 次幂
// 容器会在初始化时指定容量,实现数据元素内存预初始化
vals []eface
}
为什么要将 head
和 tail
合并到一个变量里面?
因为这样可以进行原子操作,完成两个字段的 lock free
(无锁编程) 优化。
例如:当队列中仅剩一个对象时,如果多个处理器 P 同时访问队列,如果没有进行并发限制,两个处理器 P 都可能获取到对象,这显然是不符合预期的。 那么在不引入互斥锁的前提下,sync.Pool
是如何实现临界区数据控制的呢?sync.Pool
利用了 atomic
包的提供的 CAS
操作,并发情况下两个处理器 P 都可能获取到对象,但是最终只会有一个处理器 P CAS
操作成功, 另外一个处理器操作失败,在更新 head
和 tail
两个字段的时候,也是通过 CAS + 位运算
进行操作的。
小结
通过对源代码中的数据结构进行分析,我们可以看到内部隐藏了非常多的设计技巧和对应的基础理论知识,接下来开始阅读构建于数据结构之上的具体算法。
这里再放一张数据结构图,方便读者结合算法代码进行分析。
对象归还
我们首先来看下对象归还流程,也就是如何把一个对象放入缓存池的某个队列中,从 Pool.Put
方法开始追踪代码。
func (p *Pool) Put(x any) {
...
l, _ := p.pin()
if l.private == nil {
// 优先设置私有变量
l.private = x
} else {
// 其次设置共享变量
l.shared.pushHead(x)
}
...
}
func (c *poolChain) pushHead(val any) {
d := c.head
if d == nil {
// 初始化头节点
// 对象池元素数量从 8 个开始,必须为 2 的 N 次幂
const initSize = 8
d = new(poolChainElt)
d.vals = make([]eface, initSize)
c.head = d
storePoolChainElt(&c.tail, d)
}
if d.pushHead(val) {
// 如果对象成功加入队列,直接返回
return
}
// 如果当前队列已满,分配一个新的队列 (长度是当前队列的 2 倍)
newSize := len(d.vals) * 2
if newSize >= dequeueLimit {
// 队列长度最大为 1073741824
newSize = dequeueLimit
}
// 初始化新的队列
d2 := &poolChainElt{prev: d}
d2.vals = make([]eface, newSize)
// 将头节点指向到新的队列
c.head = d2
// 将新的队列添加到链表中
storePoolChainElt(&d.next, d2)
// 将对象添加到新的队列
d2.pushHead(val)
}
poolDequeue.pushHead 方法将一个对象加入到队列中,如果队列已满,返回 false,该方法必须由 单个生产者
操作。
func (d *poolDequeue) pushHead(val any) bool {
ptrs := atomic.LoadUint64(&d.headTail)
head, tail := d.unpack(ptrs)
if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head {
// 说明队列已满 (tail 索引 + 当前队列元素个数) == head 索引
return false
}
slot := &d.vals[head&uint32(len(d.vals)-1)]
// 检测索引位置的对象是否和 popTail 方法操作冲突
typ := atomic.LoadPointer(&slot.typ)
if typ != nil {
// 有其他 goroutine 正在调用 popTail 方法操作当前位置的对象
// 所以队列实际上已满
return false
}
// 执行到这里,typ == nil
// 说明即使存在 popTail 方法操作当前位置的对象,操作也已经结束了,冲突解除
if val == nil {
val = dequeueNil(nil)
}
// 使用归还的对象填充索引位置
*(*any)(unsafe.Pointer(slot)) = val
// head 索引 + 1
atomic.AddUint64(&d.headTail, 1<<dequeueBits)
return true
}
获取对象
接下来探究从缓存池中获取对象的流程,从 Pool.Get
方法开始追踪代码。
func (p *Pool) Get() any {
l, pid := p.pin()
// 首先尝试从当前处理器的私有变量获取对象
x := l.private
// 从私有变量获取后,及时将私有变量置为 nil
l.private = nil
if x == nil {
// 私有变量没有获取到对象,尝试从共享变量获取
x, _ = l.shared.popHead()
if x == nil {
// 当前处理器 P 没有对象,尝试从其他处理器窃取
x = p.getSlow(pid)
}
}
// 如果从所有处理器的缓存池都没有获取到对象,并且 New 方法不为 nil
// 那就调用 New 方法创建一个对象返回
if x == nil && p.New != nil {
x = p.New()
}
return x
}
func (c *poolChain) popHead() (any, bool) {
d := c.head
for d != nil {
// 从队列头部开始获取对象
if val, ok := d.popHead(); ok {
return val, ok
}
// 将当前队列前驱节点作为接下来要遍历的队列
d = loadPoolChainElt(&d.prev)
}
return nil, false
}
poolDequeue.popHead 方法从队列头部删除一个对象并返回,如果队列为空,返回 false, 该方法必须由 单个生产者
操作。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
0
Pool.getSlow 方法用于当前处理器 P 没有对象时,尝试从其他处理器 P 窃取对象。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
1
poolDequeue.popTail 方法从队列尾部删除一个对象并返回,如果队列为空,返回 false, 该方法必须由 多个生产者
操作。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
2
流程图
辅助方法
pin
pin 方法绑定当前 goroutine
到处理器 P 并禁止抢占,返回一个 poolLocal
对象指针和处理器 P 的 ID。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
3
pinSlow
pinSlow 方法创建一个新的 poolLocal
对象并返回。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
4
indexLocal
indexLocal 方法根据索引参数,返回 local
数组中对应的 poolLocal
对象。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
5
缓存池 GC 过程
sync.Pool
包文件中有一个 init 函数,内部注册了 GC 执行方法。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
6
poolCleanup 方法在缓存池的清理过程中,并不会直接释放池对象,而是会将其放入 victim
中,等到下一轮清理时再释放。这样可以防止缓存池被直接释放后,变为冷启动时面对突然暴涨的对象请求造成的性能抖动,通过将缓存池放入 victim 中,可以起到避免 GC 毛刺、平滑过渡的作用。
// Pool 一旦使用后,便不能再复制
// 在 Go 内存模型术语中,调用 Put(x) 方法在调用 Get 方法之前同步
// 在 Go 内存模型术语中,调用 New 方法在调用 Get 方法之前同步
type Pool struct {
// noCopy 可以添加到 struct 中,实现 "首次使用之后,无法被复制" 的功能,主要服务于 `go vet`
// 假设一个缓存池对象 A 被对象 B 拷贝了,接着 A 被清空了,B 里面的缓存对象指针指向的对象将会不可控
noCopy noCopy
// 指向固定长度的数组,数组长度为处理器 P 的个数,转换后其实就是 [P]poolLocal 数组
// 实际的底层数据结构是切片,不过下文中统一用数组描述,读者不必在意这个细节
// 访问时根据处理器 P 的 ID (作为索引) 去访问
// 优化点: 多个 goroutine 使用同一个缓存池时,可以减少竞争,提高性能
// 类似于分段锁中降低锁粒度的设计理念
local unsafe.Pointer
// local 数组的长度
localSize uintptr
// 上一轮的 local, 内容语义和 local 一致
// 新一轮 GC 到来时,更新为当前 local 的值
victim unsafe.Pointer
// 上一轮的 localSize, 内容语义和 localSize 一致
// 新一轮 GC 到来时,更新为当前 localSize 的值
victimSize uintptr
// 创建对象的函数
New func() any
}
7
小结
通过学习 sync.Pool
的源代码,我们可以深入理解和学习到的高性能编程设计理念和技巧:
noCopy 机制 CPU CacheLine 伪共享、内存对齐 poolDequeue.headTail 字段合并设计,压缩、解压、CAS 操作、索引定位等 每个处理器 P 持有一个对象池,最大限度降低并发冲突 私有变量/共享变量 单生产者/多消费者模式实现 “读写分离” 双端队列的出队顺序 (当前处理器 P 从队列头部操作,其他处理器 P 从队列尾部操作),最大限度降低并发冲突 无锁编程 对象窃取机制 垃圾回收时的新旧对象交替使用,类似分代垃圾回收的设计理念
Reference
Go 高性能 - 无锁编程[1] Go 高性能 - sync.Pool[2] false sharing[3] victim cache[4] 多图详解 Go 的 sync.Pool 源码[5] golang 的对象池 sync.pool 源码解读[6] 深度分析 Golang sync.Pool 底层原理[7] Go 1.13 中 sync.Pool 是如何优化的?[8] 伪共享(false sharing),并发编程无声的性能杀手[9] memory barrier[10]
链接
Go 高性能 - 无锁编程: https://dbwu.tech/posts/golang_lockfree/
[2]Go 高性能 - sync.Pool: https://golang.dbwu.tech/performance/sync_pool/
[3]false sharing: https://en.wikipedia.org/wiki/False_sharing
[4]victim cache: https://en.wikipedia.org/wiki/Victim_cache
[5]多图详解Go的sync.Pool源码: https://www.luozhiyun.com/archives/416
[6]golang的对象池sync.pool源码解读: https://zhuanlan.zhihu.com/p/99710992
[7]深度分析 Golang sync.Pool 底层原理: https://www.cyhone.com/articles/think-in-sync-pool
[8]Go 1.13中 sync.Pool 是如何优化的?: https://colobu.com/2019/10/08/how-is-sync-Pool-improved-in-Go-1-13/
[9]伪共享(false sharing),并发编程无声的性能杀手: https://www.cnblogs.com/cyfonly/p/5800758.html
[10]memory barrier: https://github.com/cch123/golang-notes/blob/master/memory_barrier.md
想要了解Go更多内容,欢迎扫描下方👇关注公众号,回复关键词 [实战群] ,就有机会进群和我们进行交流

推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...