0 前言
在近期自学 c++ 过程中,我希望通过使用 c++ 复刻实现 go 中经典模块的实践方法,在增加 c++ 熟练度的同时,也进一步提升对 go 的理解程度,以此来争取做到触类旁通、一石二鸟.
本期中我的学习目标是 go 中的对象池 sync.Pool
,本期内容会分为如下三部分:
第 1 章——针对对象池技术理念与应用背景展开综述
第 2 章——针对 go 对象池 sync.Pool
进行原理解析与源码走读
第 3 章——基于 c++ 从零实现对象池 instancePool
,并展示其中的设计思路与源码细节
我在自学 c++ 过程中涉及到的实践项目都会收录于开源项目 https://github.com/xiaoxuxiansheng/cbricks.git 中,大家若觉得有所帮助,希望能留个 star,这是支持鼓励我持续创作的动力,感谢~
1 对象池应用背景
1.1 设计理念
1.1.1 性能优化策略
后端性能优化存在着经典的三板斧——异步、分治、复用.
•
异步
:例如同步任务转异步
,实现流程解耦
,类似于 MQ(Message Queue,消息队列)的设计理念 ;例如在批处理过程中,先进行异步并发处理,最后再对结果聚合汇总,类似于 go 中 sync.WaitGroup 的设计理念;•
分治
:针对高强度任务,可以化整为零
,把工作量分治到多个预处理操作
中,将蜂屯蚁聚化解为细水长流,类似于 go 中 map 渐进式扩容的设计理念;•
复用
:对于高频使用的资源类型进行循环利用,而非重复创建销毁,类似于池化技术
的设计理念.
1.1.2 池化技术权衡
聊到池化技术,想必大家并不会感到陌生,例如常见的线程池、协程池,各种类型的连接池,以及包括今天我们要重点探讨的对象池
,都是对池化技术的实际应用.
判断是否启用一项技术,其背后的选型思路
大多是在做成本上的辩证权衡
. 针对池化技术,我们所要权衡的是,在不作池化时,资源实例重复创建、销毁的成本
,以及引入池化后维护管理数据结构、交互流程
所带来的额外复杂度成本
.
因此,引入池化技术的原因是很明确的,就是资源重复创建销毁的成本太高了,大抵又可以分为两类:
•
资源昂贵
:如果资源本身创建和销毁成本就很高,那么自然希望利用池化复用技术,在执行一次创建、销毁行为的前提下,尽可能地提升资源的利用率;•
处理频繁
:如果资源创建和销毁的频次频次非常高,那么哪怕处理单个资源实例的成本较低,最终由于数量积少成多,也会形成可观的损耗,因此自然希望利用池化复用技术,来降低创建和销毁行为的执行频率
1.1.3 池化技术原理
简单总结池化技术的实现思路,其中通用的部分包括:
1)创建一个池子载体
2)需要资源的时候,从池子中获取
3)用完资源的时候,放回池子
其中可根据实现灵活调整的部分包括:池子的存储模型选型;池子是否设置容量上限;池内资源的回收机制;获取和回归资源时的交互流程设计等
1.2 对象池之于 go
1.2.1 对象池思路
在 go 中,对象池 sync.Pool
是将池化技术应用落地的经典案例. 在展开 sync.Pool 实现原理之前,我们先抛出问题的引子.
试想一个场景,在 go 程序中,我们需要在短时间内并发构造大量的对象实例
,此时由于内存分配行为导致内存使用率激增
,进一步增加了 GC(Garbage collection,垃圾回收)的执行频率,频繁的 GC
又进一步限制了程序的执行效率,使得内存释放效率更低,最终一步步形成恶性循环.
在上述场景中,已知有大量对象实例在短时间内被重复构造与回收
,倘若我们有一个资源回收中转站
,对不用的实例进行暂存
而非直接销毁,并提供循环复用
的能力而非无脑新建实例,那么就能在很大程度上降低内存分配和垃圾回收的压力,进而优化整个程序的执行效率.
对象池 sync.Pool
正是扮演了这个资源回收中转站的角色. 通过这种方式,sync.Pool 能在一定程度上预测对象的生命周期
,在对象可能被回收之前,给予它们一个被重复使用
的机会.
1.2.2 对象池应用场景
在实际工程场景中,sync.Pool 有着十分广泛的应用,比如 go 著名的 web 框架——gin 中,就对 sync.Pool 有所应用:
在基于 gin 启动的 http server 中,针对到来的 http 请求,会为之分配一个 gin.Context 实例,由于承载关于这次请求链路的上下文信息.
在这个场景中,gin.Context 就是一个可能被量产使用的工具类,其本身创建销毁成本不高,但随着 qps(Query Per-Second) 的增长,可能在短时间内被重复创建、销毁,因此很适合使用对象池技术进行缓存复用.
下面简单展示 gin 中对 sync.Pool 的使用细节,对应代码位于 gin.go 文件,使用的 gin 源码版本为 v1.9.1:
// 构建 gin.Engin 实例,其属于 http Handler 实现类
func New()*Engine{
// ...
engine :=&Engine{
// ...
}
// ...
/*
engine.pool 就是对象池,用于托管复用 gin.Context
此处声明了 pool 中针对 gin.Context 实例的构造函数
*/
engine.pool.New=func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
// 处理 http 请求
func (engine *Engine)ServeHTTP(w http.ResponseWriter, req *http.Request){
// 从对象池中获取 gin.Context 实例
c := engine.pool.Get().(*Context)
// ...
// 清空 gin.Context 实例中的内容
c.reset()
// 处理 http 请求
engine.handleHTTPRequest(c)
// 用完后,将 gin.Context 实例归还到对象池中
engine.pool.Put(c)
}
除了 gin 之外,像是 fmt 标准库中针对 printer pp,协程池三方库 ants 中针对协程对象 goworker,都应用到了 sync.Pool 技术进行管理复用,大家若感兴趣也可以自行探索求证.
1.3 对象池之于 c++
c++ 与 go 的一大差异在于没有 GC,需要由使用方更多地介入到内存管理流程中(主要指堆对象,通过 new/delete 操作).
即便 c++ 中没有 GC,其中对于对象的创建和销毁
同样涉及内存分配和释放操作
,这些都可能导致性能问题,因此在 c++ 中引入对象池技术也不失为一种好的策略,以下是对象池在 c++ 中的一些潜在应用价值:
•
减少动态内存分配
:频繁的动态内存分配(new/delete)可能导致内存碎片化以及产生性能问题. 使用对象池可以有效减少内存分配的执行频次.•
循环复用昂贵资源
:对于创建成本较高的资源,使用对象池进行缓存复用,并设置合理的空闲资源回收策略,也能有效降低因资源初始化而带来的成本.
由于 c++
没有 GC
,也不像 go 一样有着浑然天成的gmp 并发架构
,所以在对象池的实现上需要做各种因地制宜的本土改造
,这部分细节我们将放在第 3 章中详细展开.
2 go 版对象池——sync.Pool
2.1 快速上手
首先,我们介绍一下有关 sync.Pool 的具体用法:
•
pool.New
// 创建 sync.Pool 实例
var pool sync.Pool
// 声明对象构造器函数
pool.New = func() any {
return &instance{
body: []byte("{"key":"value"}"),
}
}
我们需要通过 Pool.New,声明对象实例的构造构造方法
——当 sync.Pool 中没有可复用的实例时,需要使用此方法生产新的实例.
值得一提的是,sync.Pool 本身是并发安全的结构,但是 New 函数内部的执行逻辑,需要由使用方自行保证其并发安全性.
•
pool.Get
inst, _ := pool.Get().(*instance)
通过 Pool.Get 方法可以从对象池中并发安全地获取对象实例
,该流程分为快慢两条路径:
1)快路径:若池中还有对象实例,则直接获取;
2)慢路径:若池中没有对象实例,则使用构造函数构造出来.
Get 方法返回的对象实例是 any 类型,需要由使用方自行将类型断言成预期的类型. 潜在前提是,一个 sync.Pool 实例最好只负责管理同一种类型的对象实例.
•
pool.Put
pool.Put(inst)
通过 Pool.Put 方法将对象实例并发安全地归还到对象池中
. 该操作通常可以结合 defer 指令使用,确保对象实例被归还,有机会得到复用.
Put 流程所做的仅仅是将实例放回池子,至于对象何时被释放,使用方无需关心也无法控制
(和 GC 节奏有关).
在实例被 Put 回 pool 后,sync.Pool 不会对其作任何清理操作,因此再通过 Get 获取实例时,可能得到因 Put 归还而存在残存“脏数据”的实例,也可能是使用 New 构造得到的新实例,其表现是是不稳定的.
因此,应当
将所有 sync.Pool 产出的实例视为没有任何特征和关联的模板实例
,不应该对 Put 和 Get 操作之间建立任何关联性或顺序性的假设.
使用 sync.Pool 时的推荐做法是,在 Put 前或者 Get 时,统一对实例进行数据清理,对所有实例一视同仁当做新产出的干净的模板实例.
下面是有关 sync.Pool 完整应用示例的代码:
package main
import(
"fmt"
"sync"
)
func main(){
// 1 模板类声明
type instance struct{
body []byte
}
// 2 对象池实例声明
var pool sync.Pool
// 3 对象池构造器函数声明
pool.New=func() any {
return &instance{
body:[]byte("{"key":"value"}"),
}
}
// 4 应用对象池
for i :=0; i <100000; i++{
// 4.1 从对象池中获取对象实例
inst, _ := pool.Get().(*instance)
// 4.2 使用对象实例
fmt.Printf("body: %s\n", inst.body)
// 4.3 用完对象实例后,归还到对象池中
pool.Put(inst)
}
}
2.2 实现原理
下面揭示关于 sync.Pool 的底层实现原理.
我们知道,在 go 中,gmp 架构
是一切技术理论的基石:
sync.Pool 底层的存储结构设计同样与 gmp 架构息息相关,通过为每个处理器 P 分配一个存储容器
,其中包含 P 私有的对象实例 private 和全局共享对象列表 shared
,来实现资源的统筹分配,并最大化控制并发粒度
.
有关 gmp 不是本文的重点,这里附上博客链接——Golang gmp 原理,大家感兴趣可自行展开.
结合上图,我们进一步对 sync.Pool 的核心设计要点进行阐述:
• 为每个处理器 P 分配
一私(private)一公(shared)
两部分存储介质;•
private 是 P 私有的对象实例
,当前 P 可以完全无锁化访问 private
. 无论是 Get 还是 Put 流程,都优先使用 P 的 private 资源;•
shared 是 P 下的共享对象实例列表
,当前 P 访问 shared 时也需要通过CAS(Compare-And-Swap) 操作保证并发安全
,这是因为其他 P 可能也会尝试来此窃取资源,因此存在并发行为.
sync.Pool 依附着 gmp 架构设计了存储结构,巧妙地以 P 作为粒度划分的尺度,通过私有域和共享域的边界划分,实现资源统筹,尽可能减小并发粒度.
2.3 源码走读
聊完理论,下面进入 sync.Pool 源码走读环节. 本文使用到的 go 源码版本是 v1.19.
2.3.1 核心数据结构
在宏观架构上,sync.Pool 定义了如下核心类型:
•
sync.Pool
:对象池主类. 其中包含的local 和 victim 是对象实例存储容器
,类型均为poolLocal 数组
,数组长度和 P 的数量一致
;•
poolLocal/poolLocalInternal
:单个 P 下的对象实例存储容器
,包含一个私有实例 private
,以及一个公有实例列表 shared
•
poolChain
:公有实例列表 shared 的实际类型,是一个双向链表
,其中节点类型是 poolChainElt•
poolChainElt
:poolChain 中的节点类型. 在存储结构上是个环形数组
,因此能同时存储多个实例
有关 sync.Pool 、poolLocal、poolLocalInternal 类的源码位于 sync/pool.go:
// A Pool is a set of temporary objects that may be individually saved and
// retrieved.
/*
对象池——适用于大量临时对象实例被反复创建和销毁的场景
Get: 从池中获取对象实例
Put: 将用完的对象实例归还到池中
New: 生成对象实例的构造函数
这个机制有助于减少因为频繁创建和销毁对象而导致的 GC 压力,同时也能够提高对象复用率,从而提升程序的性能。
通过这种方式,sync.Pool 能够在一定程度上预测对象的生命周期,并在对象可能被回收之前,给予它们一个被重新使用的机会
*/
typePoolstruct{
// 禁用拷贝操作
noCopy noCopy
/*
当前轮次的池容器指针
真实类型是 [P]poolLocal,数组容量与 P 的个数相等
*/
localunsafe.Pointer// local fixed-size per-P pool, actual type is [P]poolLocal
// local 的容量. 与 P 的个数相等
localSize uintptr// size of the local array
/*
上个轮次的池容器指针
真实类型是 [P]poolLocal,数组容量与 P 的个数相等
*/
victim unsafe.Pointer// local from previous cycle
// victim 的容量. 与 P 的个数相等
victimSize uintptr// size of victims array
// 构造器函数. 当池子为空时,使用此函数产生新的对象实例
Newfunc() any
}
上述代码的大部分内容都以通过注释加以说明,此处大家可能存有疑惑的点在于
local 和 victim 之间的关系
.
victim 和 local 类型完全一致,都是长度与 P 数量相等的 poolLocal 数组,但在时序关系上,
victim 是 local 在一个轮次后演进轮换得到的产物
.
那么如何理解此处有关轮次的概念呢?此处指的是 go 中的
GC 轮次
. 每当发生 GC 时,会将上一轮的 victim 清空,然后将 local 更迭置为下一轮的 victim.
通过这样的迭代演进机制,保证经过两轮 GC 后,Pool 中没有被重新复用的对象实例会被回收. 这部分内容我们还将在 2.3.5 小节中作展开介绍.
type poolLocal struct{
poolLocalInternal
// ...
}
// Local per-P Pool appendix.
// 与每个 P 一一对应的对象存储容器
type poolLocalInternal struct{
// 每个 P 私有的单个对象实例存储凹槽
private any // Can be used only by the respective P.
// 与其他 P 共享的对象实例存储链表
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
poolLocalInternal 对应为每个处理器 P 下的实例存储容器
,包含一私(private)一公(shared)两部分内容:
•
private
:存放单个实例的凹槽,属于某个 P 私有,可以无锁访问•
shared
:某个 P 下共享实例列表,名义上属于某个 P,但还可能被其他 P 窃取,所以访问时需要使用 CAS
有关 poolChain、poolDequeue 的类定义代码位于 sync/poolqueue.go 文件中:
// 各 P 中存储共享对象实例的双向链表
type poolChain struct {
// 链表的头节点
head *poolChainElt
// 链表的尾节点
tail *poolChainElt
}
poolChain 是 shared 共享实例列表的实际类型
,是个双向链表,通过 head/tail 持有头尾节点的指针:
// 封装对象实例的节点
type poolChainElt struct {
// 节点内存储对象实例的环形队列
poolDequeue
/*
相邻节点
next——下一个节点
prev——前一个节点
*/
next, prev *poolChainElt
}
poolChainElt 为 poolChain 链表中的实际的节点类型
,其中实例存储在嵌套的 poolDequeue 环形队列中:
type poolDequeue struct{
// 记录环形数组头尾索引信息
headTail uint64
// 存储的对象实例
vals []eface
}
// 只有当前 P 往 shared 中追加对象实例时,使用此方法
func (d *poolDequeue) pushHead(val any)bool{
// ...
}
// 只有当前 P 尝试从 shared 中获取对象实例时,使用此方法
func (d *poolDequeue) popHead()(any,bool){
// ...
}
// 任意 P 可能使用此方法,尝试从当前 P shared 中窃取对象实例
func (d *poolDequeue) popTail()(any,bool){
// ...
}
poolDequeue 是一个基于 CAS 操作访问的无锁化环形队列,底层基于数组实现,并通过双指针 head tail 实现逻辑意义上的环形遍历
.
poolDequeue 面向单生产者(当前 P)、多消费者(所有 P),当前 P 会通过 pushHead 和 popHead 访问 shared,以头节点 head 为起点进行遍历;其他 P 在窃取过程中会通过 popTail 方法访问 shared,以尾节点 tail 为起点进行遍历.
2.3.2 获取对象实例
从 sync.Pool 中获取对象实例的方法为 Pool.Get
,包含如下核心步骤:
• 通过
pin
操作,将当前 G 和 P 绑定在一起,禁用抢占行为
(pin 流程);• 尝试获取当前 P 对应 local 中的
private 私有实例(无并发)
;• 尝试从当前 P 对应
local
的shared 共享列表(CAS)
中获取实例(popHead 流程);• 尝试从
其他 P
对应 local 的shared 共享列表(CAS)
中窃取实例(getSlow 流程-I);• 尝试获取当前 P 对应
victim
中的private 私有实例(无并发)
(getSlow 流程-II);• 尝试从
其他 P
对应victim
的shared 共享列表(CAS)
中窃取实例(getSlow 流程-III);• 执行
unpin
操作,对当前 G 和 P 解绑;• 如果还未获得实例,且 New 非空,执行
New 构造实例
;• 返回实例
/*
从对象池中获取一个对象实例. 对象实例之间不应该存在任意差异性与关联性
1)针对当前轮次 local 容器进行操作
1-1)尝试获取当前 P 私有的对象实例 private(无锁)
1-2)尝试从当前 P 共享对象实例队列 shared 中获取对象实例
2)针对上一轮 victim 容器进行操作
2-1)尝试获取当前 P 私有的对象实例 private(无锁)
2-2)尝试从当前 P 共享对象实例队列 shared 中获取对象实例
3)使用构造函数 New 生产出新的对象实例
*/
func (p *Pool)Get() any {
// ...
// 将当前 G 和 P 使用“别针”绑定到一起(禁止抢占),并返回 P 对应的 local 容器
l, pid := p.pin()
// 尝试获取 local 容器中当前 P 私有的 private 实例
x := l.private
l.private=nil
if x ==nil{
// 尝试从当前 P 共享 shared 中获取对象实例,从头部开始遍历
x, _ = l.shared.popHead()
if x ==nil{
//
x = p.getSlow(pid)
}
}
// 与 pin 操作对偶的解绑操作
runtime_procUnpin()
// ...
// 至此仍未获得有效的对象实例,则使用构造函数进行构造
if x ==nil&& p.New!=nil{
x = p.New()
}
return x
}
尝试从当前 P 对应 local 的 shared 共享队列中获取实例
的环节中,会调用 popHead
方法——沿着 shared 头节点 head 开始遍历获取实例,每个节点是 poolChainElt 类型,在节点的 popHead 方法中存在 CAS 操作.
// 从 shared 头部开始遍历获取对象实例
func (c *poolChain) popHead()(any,bool){
// 从 shared 头部节点开始尝试
d := c.head
for d !=nil{
// 尝试从节点中获取实例. 内部基于 cas 操作
if val, ok := d.popHead(); ok {
return val, ok
}
// 沿着 shared 头节点向尾节点移动
d = loadPoolChainElt(&d.prev)
}
return nil,false
}
从当前 P 对应 local 中获取实例失败之后,会执行 getSlow
流程,包含如下步骤:
• 尝试从各个 P 对应 local 的 shared 中窃取实例,在顺序上从当前 P 的下一个 P 开始(根据 pid 排序)
• 尝试获取当前 P 对应 victim 的 private 实例
• 尝试从各个 P 对应 victim 的 shared 中窃取对象实例,在顺序上从当前 P 开始(根据 pid 排序)
• 若至此仍未获得实例,说明为 victim 是空的,将 victimSize 置为 0,提示后续流程无需重复遍历
func (p *Pool) getSlow(pid int) any {
// 读取全局 locals 数组的长度
size := runtime_LoadAcquintptr(&p.localSize)// load-acquire
// 获取全局 locals 数组
locals := p.local// load-consume
// 沿着 pid + 1 的位置开始,循环遍历 locals 数组中各个 p 对应的 local
for i :=0; i <int(size); i++{
// 尝试从指定 p 的 shared 中获取对象实例,从尾部开始遍历
l := indexLocal(locals,(pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x !=nil{
return x
}
}
// 从 locals 中获取对象实例失败,则需要尝试从 victim 中获取
size = atomic.LoadUintptr(&p.victimSize)
// ...
locals = p.victim
// 尝试从当前 p 对应的 victim 中获取私有对象实例
l := indexLocal(locals, pid)
if x := l.private; x !=nil{
l.private=nil
return x
}
// 从 pid 位置开始,遍历 victims 数组中各个 p 对应的 victim
for i :=0; i <int(size); i++{
// 尝试从指定 p 的 shared 中获取对象实例,从尾部开始遍历
l := indexLocal(locals,(pid+i)%int(size))
if x, _ := l.shared.popTail(); x !=nil{
return x
}
}
// 若 victims 中没有对象实例,直接将其长度置为 0,避免后续重复执行无意义的遍历行为
atomic.StoreUintptr(&p.victimSize,0)
return nil
}
popTail 方法发生在对所有 P 的无差别窃取流程
中,该方法会沿着 shared 的尾节点 tail 开始遍历获取实例,每个节点是 poolChainElt 类型,在节点的 popTail 方法中会存在 CAS 操作.
// 从 shared 尾部开始遍历获取对象实例
func (c *poolChain) popTail()(any,bool){
// 以 shared 尾部作为起点
d := loadPoolChainElt(&c.tail)
if d ==nil{
return nil,false
}
for{
// 自尾部向前遍历
d2 := loadPoolChainElt(&d.next)
// 尝试从当前节点中获取对象实例
if val, ok := d.popTail(); ok {
return val, ok
}
// 来到终点,退出循环
if d2 ==nil{
return nil,false
}
// tail 中未获得对象实例,则更新 tail 指针
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.tail)),unsafe.Pointer(d),unsafe.Pointer(d2)){
storePoolChainElt(&d2.prev,nil)
}
d = d2
}
}
2.3.3 归还对象实例
向 sync.Pool 中归还对象实例的方法为 Pool.Put
,包含如下核心步骤:
• 通过
pin
操作,将当前 G 和 P 绑定在一起,禁用抢占行为
(pin 流程);• 尝试将对象实例放置在当前 P 对应
local
的private 位置(无并发)
;• 将对象实例追加到当前 P 对应
local
的shared 中(CAS)
;• 执行
unpin
操作,对当前 G 和 P 解绑.
/*
往对象池中归还一个对象实例. 对象实例之间不应该存在任意差异性与关联性
1)尝试放置到当前 P 对应 local 的 private 中
2)放置到当前 P 对应 local 的 shared 的头部
*/
func (p *Pool)Put(x any){
if x ==nil{
return
}
// ...
// 将 G 与 P 绑定在一起,禁止抢占
l, _ := p.pin()
// 若当前 P 对应 local 的 private 为空,直接放置
if l.private==nil{
l.private= x
}else{
// 放回到当前 P 对应 local 的 shared 头部中
l.shared.pushHead(x)
}
// G 与 P 解绑
runtime_procUnpin()
// ...
}
pushHead 方法用于将实例追加到共享列表 shared
中:
• 如果 shared 的头节点 head 为空,则对其进行初始化
• 如果头节点 head 空间充足,则将对象实例追加到其中(CAS)
• 如果头节点已满,则扩大单个节点容量并构造新的头节点 head,然后将对象实例追加到其中(CAS)
// 将对象实例放置到 shared 头部中
func (c *poolChain) pushHead(val any){
// 获取 shared 头部节点
d := c.head
// 若头节点为空,进行初始化
if d ==nil{
const initSize =8// Must be a power of 2
d =new(poolChainElt)
d.vals =make([]eface, initSize)
c.head = d
storePoolChainElt(&c.tail, d)
}
// 将对象实例放置到头节点中
if d.pushHead(val){
return
}
// 若头节点满了,以双倍容量构造新的头节点,并进行对象实例放置
newSize :=len(d.vals)*2
// 单个节点容量存在上限
// (1 << dequeueBits) / 4 // 1073741824
if newSize >= dequeueLimit {
// Can't make it any bigger.
newSize = dequeueLimit
}
d2 :=&poolChainElt{prev: d}
d2.vals =make([]eface, newSize)
c.head = d2
storePoolChainElt(&d.next, d2)
d2.pushHead(val)
}
2.3.4 绑定与初始化
无论是 Get 还是 Put 操作,都会触发 pin
流程,其中会调用 runtime_procPin
指令,将 G 与 P 绑定在一起,并返回当前 P 对应的 poolLocal 以及 id;
除此之外,当一个 sync.Pool 实例在一轮 GC 后首次执行 pin 流程
时,会进入到 pinSlow
流程中,对当前 sync.Pool 实例完成初始化操作:
/*
将 g 与 p 绑定在一起,禁用抢占模式
返回当前 p 对应的 local,以及当前 p 对应的 pid
*/
func (p *Pool) pin()(*poolLocal,int){
// 执行 pin 操作
pid := runtime_procPin()
// 读取 locals 数组长度
s := runtime_LoadAcquintptr(&p.localSize)// load-acquire
l := p.local// load-consume
// 常规分支——pid 为 locals 数组中的合法 index,因此 pid < s
if uintptr(pid)< s {
// 返回 p 对应的 local,以及 p 的 id
return indexLocal(l, pid), pid
}
// 倘若全局 locals 数组未初始化,或者 p 数量发生了改变,则走 pinSlow 分支
return p.pinSlow()
}
在 pinSlow
方法中,包含如下核心步骤:
• 先执行
unpin
操作,因为马上需要加锁,所以暂时将 G 置为可抢占状态• 加全局维度锁
allPoolsMu
• 重新执行
pin
操作,将 G 置为不可抢占状态• 加锁后
double check
,如果此期间当前 sync.Pool 实例已经完成过初始化,直接返回• 将当前
sync.Pool 实例追加到全局的 allPools 列表
中•
初始化
当前 sync.Pool 实例的local 列表
• 返回当前 P 对应 poolLocal 以及 id
// 对 locals 进行初始化,并在 当前 sync.Pool 实例首次调用该方式,将其添加到全局的 allPools 中
func (p *Pool) pinSlow()(*poolLocal,int){
/*
由于接下来要全局维度锁,所以需要先解除 p 与 g 的 pin 状态
*/
runtime_procUnpin()
// 加全局维度的互斥锁
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
// 重新将 g 和 p pin 在一起
pid := runtime_procPin()
// 加锁后 double check
s := p.localSize
l := p.local
// 若 locals 已经被初始化,则直接返回
if uintptr(pid)< s {
return indexLocal(l, pid), pid
}
// 若一轮 GC 后首次对当前 sync.Pool 实例调用 pinSlow 方法,则需要将其添加到全局的 allPools 中
if p.local==nil{
allPools =append(allPools, p)
}
// 对 locals 进行初始化
size := runtime.GOMAXPROCS(0)
local:=make([]poolLocal, size)
atomic.StorePointer(&p.local,unsafe.Pointer(&local[0]))// store-release
runtime_StoreReluintptr(&p.localSize,uintptr(size))// store-release
// 返回当前 p 对应的 local,以及 p 对应的 id
return &local[pid], pid
}
2.3.5 清理对象实例
在 sync.Pool.go 文件的 init
方法中,会将 poolCleanup 方法
注册到 GC 执行回调函数
中,保证每次程序启动 GC 时,都会先执行 poolCleanup 方法:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
func poolCleanup(){
// 遍历上一轮的 sync.Pool 实例,将其 victims 清空
for _, p :=range oldPools {
p.victim =nil
p.victimSize =0
}
// 遍历这一轮的 sync.Pool 实例,将 locals 更新到 victims 中
for _, p :=range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local=nil
p.localSize =0
}
// 将 allPools 更新到 oldPools 中
oldPools, allPools = allPools,nil
}
poolCleanup 函数的调用逻辑为:
• 以 GC 轮次划分,实现
allPools -> oldPools
的轮换• 以 GC 轮次划分,实现
local -> victim
的迭代
因此,对于一个 sync.Pool 实例,回收的实例首先会放入到 local 中,并在至多经历 2 轮 GC 后,丢失其引用.
sync.Pool 的存储设计和回收流程中借鉴了受害者缓存 victim cache
的设计理念,旨在结合提高热缓存数据的命中率:
• 存在主缓存 local 和受害者缓存 victim
• 当主缓存 local 数据 miss 时,可使用受害者缓存 victim 中的替补数据
• 如果 victim 中替补数据被使用到,则归还时会重新先进入 local
• 在回收缓存时,按照轮次的节奏,将当前轮的 local 替换为下一轮的 victim
3 c++ 版对象池——instancePool
3.1 设计思路
从本章开始,进入到 c++ 实践项目的介绍——基于 c++ 从零到一实现对象池工具 InstancePool
.
我们再次明确 c++ 与 go 的差异:
•
没有 gmp
:以线程 thread 为最小调度单元,即便使用协程 coroutine,由于缺少类似 gmp 这样并发生态的支持,在并发粒度上也存在诸多限制•
没有 GC
:使用方需要关心内存管理与释放的时机
,但可以通过引入智能指针
技术使该问题得到一定程度的改善
综上,我们在效仿 go sync.Pool 的同时,也基于 c++ 11 的风格,对 InstancePool 进行因地制宜的改造:
•
设定层级 level 的概念
:各操作流程基于不同的 level 进行资源共享,通过 level 层级划分的方式减小并发粒度(效仿 sync.Pool 基于 P 分级的设定);•
建立定时回收任务
:定期清理对池中实例的共享指针引用,以定时任务的执行作为 local -> victim 的迭代间隔;(效仿 sync.Pool 基于 GC 轮次迭代的设定)
此外,instancePool 尽可能借鉴 sync.Pool 的其他设计思路,包括:
• 应用
victim cache
设计理念,将存储容器划分为 local -> victim 两个层级;• 各 level 下的存储容器,分为
一私(single)一公(shared)
两部分介质• 获取实例优先取 single,其次取 shared,最后对其他 level 发起
窃取 stealing
操作
3.2 源码实现
下面进入 instancePool 实现源码介绍环节,项目完全基于 c++ 11 实现,并已开源于 https://github.com/xiaoxuxiansheng/cbricks.git 中. 对象池头文件为 ./pool/instancepool.h,实现文件为 ./pool/instancepool.cpp.
3.2.1 依赖的基础能力
实现 instancepool 过程中,依赖到一些更底层的基础能力,包括标准库以及自制库两部分:
• 标准库:
// 时间相关
#include <chrono>
// 函数编程相关
#include <functional>
// 智能指针相关
#include <memory>
// 队列
#include <queue>
// 原子变量、原子操作
#include <atomic>
• 自制库:
// 禁用类的拷贝、赋值操作
#include "../base/nocopy.h"
// 效仿 golang defer 机制. 利用栈内变量析构机制,保证栈回收前执行指定任务
#include "../base/defer.h"
// 锁相关,包含自旋锁 spinLock 互斥锁 lock
#include "../sync/lock.h"
// 线程相关
#include "../sync/thread.h"
// 信号量相关
#include "../sync/sem.h"
3.2.2 头文件定义
在 InstancePool 头文件
定义中,包含三个核心模块:
•
Instance
:包含纯虚函数
的对象实例 interface
,使用方需要实现该 interface
,定义特定的实例类型•
InstancePool
:对象池类. 持有构造实例函数 m_constructF
;当前轮与上一轮存储对象实例容器 m_local 和 m_victim
;回收任务执行间隔 m_evictInterval. 其他更多细节参见代码注释•
LevelPool
:每个level 下的存储容器
,可类比为 sync.Pool 中的 poolLocal,其中包含一个level 私有的对象实例 single
以及一个公有的实例链表 shared
,分别通过自旋锁
和互斥锁
加以保护
namespace cbricks{namespace pool{
/**
* 对象实例的 interface 定义
*/
class Instance{
public:
typedef std::shared_ptr<Instance> ptr;
public:
Instance()=default;
/**
* @brief:析构函数 置为 virtual,保证继承类的析构函数能被正常调用
*/
virtual~Instance()=default;
/**
* @brief:重置实例中的数据——纯虚函数,需要被实现
*/
virtual void clear() =0;
};
/**
* 对象池,不可值拷贝和值复制
*/
class InstancePool: base::Noncopyable{
public:
// 实例构造函数 类型别名
typedef std::function<Instance::ptr()> constructF;
// 毫秒 类型别名
typedef std::chrono::milliseconds ms;
// 方法栈回收前的兜底执行函数
typedef base::Defer defer;
/** 并发相关 */
// 线程 类型别名
typedef sync::Thread thread;
// 信号量 类型别名
typedef sync::Semaphore semaphore;
// 自旋锁 类型别名
typedef sync::SpinLock spinLock;
// 互斥锁 类型别名
typedef sync::Lock lock;
public:
/**
* @brief:构造函数
* @param:_constructF——实例构造函数
* @param:level——资源粒度分级,默认分成 8 份
* @param:expDuration——过期回收时间,表示 instance 在对象池中闲置多长时间后会被自动回收
*/
InstancePool(constructF _constructF,constint level =8,const ms expDuration =ms(500));
/** 析构函数 */
~InstancePool();
private:
/**
* 某个 level 下的 instance 队列
*/
structLevelPool{
// 共享指针 类型别名
typedef std::shared_ptr<LevelPool> ptr;
// 保护私有 instance 实例的自旋锁
spinLock singleLock;
// 私有 instance 实例
Instance::ptr single =nullptr;
// 保护共享 instance 队列的互斥锁
lock sharedLock;
// 共享 instance 队列
std::queue<Instance::ptr> shared;
};
public:
/**
* @brief: 从 instance pool 中获取一个 instance 实例:
* - 0)根据 level 递增计数器,分配得到一个 level
* - 针对 m_local 操作:
* - 1)根据 level 获取对应的 levelPool
* - 2)尝试获取当前 levelPool 的 private instance( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 3)尝试从当前 levelPool 的 shared 队列中获取 instance(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 4) 尝试从其他 levelPool 的 shared 队列中获取 instance(加其他 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 5)使用 newFc 构造出新实例
* - 针对 m_victim 操作:
* - 6)根据 level 获取对应的 levelPool
* - 7)尝试获取当前 levelPool 的 private instance( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 8)尝试从当前 levelPool 的 shared 队列中获取 instance(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 9) 尝试从其他 levelPool 的 shared 队列中获取 instance(加其他 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 10)使用 newFc 构造出新实例
* @return: 返回获取到的实例
*/
Instance::ptr get();
/**
* @brief:将一个 instance 归还回到 instancePool
* @param: instance——归还的 instance
* - 1)根据 level 递增计数器,分配得到一个 level
* - 2)根据 level 从 m_local 获取对应的 levelPool
* - 3)尝试放置到当前 levelPool 的 private 中( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 4)放置到当前 levelPool 的 shared 队列(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
*/
void put(Instance::ptr instance);
private:
/**
* @brief: [异步执行] 定时清理达到过期时长的 instance
* - 1)每隔 expDuration/2 执行一次
* - 2)构造新的 m_local 实例
* - 3)回收 m_victim
* - 4)将前一轮的 m_local 变为 m_victim
* - 5)将新的 m_local 变为 m_local
*/
void asyncEvict();
/**
* @brief: 从某个指定 levelPool 中获取 instance 实例
* @param:levelPool——指定的 levelPool
* @param:获取 instance 过程中,是否忽略 levelPool 中的 single instance 实例. 默认为 false
*/
Instance::ptr getFromLevelPool(LevelPool::ptr levelPool, const bool ignoreSingle = false);
/**
* @brief: 从某个 pool 的指定 level 中获取 instance 实例
* @param:pool——m_local 或者 m_victim
* @param:level——指定 level 层级
*/
Instance::ptr getFromPool(std::vector<LevelPool::ptr>& pool, int level);
private:
// evict thread 轮询间隔
ms m_evictInterval;
// 用于构造 instance 实例的构造器函数
constructF m_constructF;
// 当前轮次的 instance 缓冲池
std::vector<LevelPool::ptr> m_local;
// 上一轮次的 instance 缓冲池
std::vector<LevelPool::ptr> m_victim;
// level 计数器
std::atomic<int> m_level{0};
int m_levelSize;
// 标识 instance pool 是否关闭
std::atomic<bool> m_closed{false};
// 信号量,用于在 instance pool 析构时控制保证 evict thread 先行退出
semaphore m_sem;
};
}}
站在使用方的视角,要做的事情包括:
• 对实例基类 Instance 进行实现;
• 在初始化 InstancePool 时传入构造实例的方法 _constructF;
• 调用 get 方法获取实例
• 调用 put 方法归还实例
3.2.3 构造&析构
InstancePool 提供了唯一的构造函数
,入参为:
•
_constructF
:对象实例的构造函数. 当 InstancePool 内没有可复用的实例时,使用此方法构造实例
•
level
:将全局存储的对象实例分成几份,用于细化并发粒度
. level 默认值为 8•
expDuration
:InstancePool 中的实例在闲置多长时间后会被“回收”(丢弃共享指针引用
).默认值为 500 ms
namespace cbricks{namespace pool{
/**
* @brief:构造函数
* @param:_constructF——实例构造函数
* @param:level——资源粒度分级,默认分成 8 份
* @param:expDuration——过期回收时间,表示 instance 在对象池中闲置多长时间后会被自动回收
*/
InstancePool::InstancePool(constructF _constructF,constint level,const ms expDuration):m_constructF(_constructF),m_levelSize(level){
CBRICKS_ASSERT(_constructF !=nullptr,"constructor is empty");
CBRICKS_ASSERT(level >0,"level is nonpositive");
CBRICKS_ASSERT(expDuration.count(),"expDuration is nonpositive");
// evict thread 轮询间隔为用户设定 instance 过期时长 expDuration 的一半
int64_t ecivtInterval = expDuration.count()/2;
if(ecivtInterval <=0){
ecivtInterval =1;
}
this->m_evictInterval =ms(ecivtInterval);
// 完成 local 中指定数量 levelPool 的初始化
this->m_local.reserve(level);
for(int i =0; i < level; i++){
this->m_local.push_back(LevelPool::ptr(newLevelPool));
}
// 异步启动 evict thread
thread thr(std::bind(&InstancePool::asyncEvict,this));
}
// ...
}}
在构造函数中,会按照 level 值,对 m_local 中的各个 levelPool 实例进行初始化;接着异步启动一个 evict thread
,用于定时执行回收任务
.
// 析构函数
InstancePool::~InstancePool(){
// 将 m_closed 标识置为 true. evict thread 感知到此信息后会自行退出
this->m_closed.store(true);
// 确保 evict thread 退出后再完成析构. m_sem.notify 方法在 evict thread 退出前执行
this->m_sem.wait();
}
在析构函数中,会将 m_closed 标识置为 true,用于通知 evict thread 退出;继而通过执行信号量 m_sem 的 wait 方法,保证在 evict thread 退出后,整个 instancePool 才会完成析构.
3.2.4 获取实例
通过 get
方法实现从 instancePool 中获取对象实例
,包含核心步骤为:
•
递增 m_level 原子计数器
,为当前操作分配一个 level 值
• 尝试获取
当前 level
对应local
的私有实例 single(加 level 粒度自旋锁 spinLock,只在同 level 间竞争)
• 尝试从
当前 level
对应local
的共享队列 shared 中获取实例(加 level 粒度互斥锁 sharedLock,不同 level 间也存在竞争)
• 尝试从
其他 level
对应local
的共享队列 shared 中获取实例(加 level 粒度互斥锁 sharedLock,不同 level 间也存在竞争)
• 尝试获取
当前 level
对应victim
的私有实例 single(加 level 粒度自旋锁 spinLock,只在同 level 间竞争)
• 尝试从
当前 level
对应victim
的共享队列 shared 中获取实例(加 level 粒度互斥锁 sharedLock,不同 level 间也存在竞争)
• 尝试从
其他 level
对应victim
的共享队列 shared 中获取实例(加 level 粒度互斥锁 sharedLock,不同 level 间也存在竞争)
• 如果经历上述流程仍未获得实例,则基于
构造函数 m_constructF 创建一个新的实例
• 调用实例
clear 方法
,确保数据清空
后,返回实例
/**
* @brief: 从 instance pool 中获取一个 instance 实例:
* - 0)根据 level 递增计数器,分配得到一个 level
* - 针对 m_local 操作:
* - 1)根据 level 获取对应的 levelPool
* - 2)尝试获取当前 levelPool 的 private instance( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 3)尝试从当前 levelPool 的 shared 队列中获取 instance(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 4) 尝试从其他 levelPool 的 shared 队列中获取 instance(加其他 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 5)使用 newFc 构造出新实例
* - 针对 m_victim 操作:
* - 6)根据 level 获取对应的 levelPool
* - 7)尝试获取当前 levelPool 的 private instance( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 8)尝试从当前 levelPool 的 shared 队列中获取 instance(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 9) 尝试从其他 levelPool 的 shared 队列中获取 instance(加其他 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
* - 10)使用 newFc 构造出新实例
* @return: 返回获取到的实例
*/
Instance::ptr InstancePool::get(){
// 获取 level 层级
int level =(this->m_level++)%this->m_levelSize;
// 从 m_local 中获取
Instance::ptr got =this->getFromPool(this->m_local,level);
if(!got){
// 从 m_victim 中获取
got =this->getFromPool(this->m_victim,level);
if(!got){
// 使用 constructF 兜底
got =this->m_constructF();
}
}
// 返回 instance 实例前进行 reset 重置
got->clear();
return got;
}
/**
* @brief: 从某个 pool 的指定 level 中获取 instance 实例
* @param:pool——m_local 或者 m_victim
* @param:level——指定 level 层级
*/
Instance::ptr InstancePool::getFromPool(std::vector<LevelPool::ptr>& pool, int level){
if(pool.empty()){
return nullptr;
}
// 从当前 level 的 levelPool 中获取. 会分别尝试 single 和 shared
Instance::ptr got =this->getFromLevelPool(pool[level]);
if(got){
return got;
}
// 从其他 level 的 levelPool 中获取. 只尝试 shared.
for(int i =0; i < pool.size(); i++){
if(i == level){
continue;
}
got =this->getFromLevelPool(pool[i],true);
if(got){
return got;
}
}
return nullptr;
}
/**
* @brief: 从某个指定 levelPool 中获取 instance 实例
* @param:levelPool——指定的 levelPool
* @param:获取 instance 过程中,是否忽略 levelPool 中的 single instance 实例. 默认为 false
*/
Instance::ptr InstancePool::getFromLevelPool(LevelPool::ptr levelPool, bool ignoreSingle){
// 尝试从当前 levelPool 的 single 中获取 instance 实例
if(!ignoreSingle){
spinLock::lockGuard guard(levelPool->singleLock);
if(levelPool->single){
Instance::ptr got = levelPool->single;
levelPool->single =nullptr;
return got;
}
}
// 尝试从当前 levelPool 的 shared 中获取 instance 实例
lock::lockGuard guard(levelPool->sharedLock);
if(!levelPool->shared.empty()){
Instance::ptr got = levelPool->shared.front();
levelPool->shared.pop();
return got;
}
return nullptr;
}
3.2.5 归还实例
使用 put
方法将对象实例归还到 instancePool
中,包含核心步骤为:
•
递增 m_level 原子计数器
,为当前操作分配一个 level 值
• 尝试将对象实例放置在
当前 level
对应local
的私有实例 single 位置(加 level 粒度自旋锁 spinLock,只在同 level 间竞争)
• 将对象实例追加到
当前 level
对应local
的共享队列 shared
中(加 level 粒度互斥锁 sharedLock,不同 level 间也存在竞争)
/**
* @brief:将一个 instance 归还回到 instancePool
* @param: instance——归还的 instance
* - 1)根据 level 递增计数器,分配得到一个 level
* - 2)根据 level 从 m_local 获取对应的 levelPool
* - 3)尝试放置到当前 levelPool 的 private 中( 加 level 粒度 spinLock,只有同 level 下的 get 和 put 行为会发生竞争 )
* - 4)放置到当前 levelPool 的 shared 队列(加 level 粒度 lock,可能和任意 level 的 get 和 put 行为发生竞争)
*/
void InstancePool::put(Instance::ptr instance){
// 获取 level 层级对应的 levelPool
LevelPool::ptr levelPool =this->m_local[(this->m_level++)%this->m_levelSize];
{
// 加自旋锁并尝试放置到 levelPool->single
spinLock::lockGuard guard(levelPool->singleLock);
if(!levelPool->single){
levelPool->single = instance;
return;
}
}
// 加互斥锁,并追加到 levelPool->shared 中
lock::lockGuard guard(levelPool->sharedLock);
levelPool->shared.push(instance);
}
3.2.6 定时清理
通过异步的 evict thread 定时“回收”对象实例
的流程.
在 instancePool 中,所有对象实例都是通过 c++ 11 中的
共享指针 shared_ptr
管理的,所以所谓"回收"
,指的是丢弃 instancePool 中持有的 shared_ptr 引用
. 而实例真正的内存释放实际上发生在 shared_ptr 计数器清零的时候. 换言之,如果此时除 InstancePool 外还存在其他位置持有 shared_ptr 引用,那么实例此时还不会发生内存释放.
这一点其实和 go 中 sync.Pool 时类似的,GC 最终的执行是依赖于三色标记法的可达性分析,而并非当 sync.Pool 丢弃 victim 时,其中的实例就会直接释放.
在 asyncEvict 方法中:
• 每隔 m_evictInterval 时长,执行一次定时回收任务
• 实现
m_local -> m_victim
的轮换• 当感知到 m_closed 标识为 true 时,主动退出 thread
在 evict thread 退出前,通过 defer 操作,保证会执行一次 m_sem.notify 方法,这样 InstancePool 才能正常完成析构.
/**
* @brief: [异步执行] 定时清理达到过期时长的 instance
* - 1)每隔 expDuration/2 执行一次
* - 2)构造新的 m_local 实例
* - 3)回收 m_victim
* - 4)将前一轮的 m_local 变为 m_victim
* - 5)将新的 m_local 变为 m_local
*/
void InstancePool::asyncEvict(){
// thread 退出前必须执行一次 sem.notify.
defer d([this](){
this->m_sem.notify();
});
// 在 instance pool 未关闭前持续运行
while(!this->m_closed.load()){
// 每间隔 expDuration/2 执行一次
std::this_thread::sleep_for(this->m_evictInterval);
// 构造新的 local
std::vector<LevelPool::ptr> newLocal;
newLocal.reserve(this->m_local.size());
for(int i =0; i <this->m_local.size();i++){
newLocal.push_back(LevelPool::ptr(newLevelPool));
}
// 新老 m_local/m_victim 轮换
this->m_victim =this->m_local;
this->m_local = newLocal;
}
}
3.3 应用示例
最后给出 InstancePool 的使用示例代码:首先实现 Instance 基类,定义其子类 demo,实现对应的纯虚方法—— clear:
// demo——instance 实现类
classdemo:public cbricks::pool::Instance{
public:
// 默认构造函数
demo(){
LOG_INFO("construct...");
}
// 默认析构函数
~demo(){
LOG_INFO("destruct...");
}
// [必须实现] 置空函数
void clear() override{
// ...
}
};
在 testInstancePool 方法中,异步启动 10000 个 thread,并发地从 instancePool 中获取对象和归还对象:
// 测试对象池代码示例
void testInstancePool(){
// 对象实例 类型别名
typedef cbricks::pool::Instance instance;
// 对象池 类型别名
typedef cbricks::pool::InstancePool instancePool;
// 信号量 类型别名
typedef cbricks::sync::Semaphore semaphore;
// 线程 类型别名
typedef cbricks::sync::Thread thread;
// 初始化日志模块,方便后续调试工作
cbricks::log::Logger::Init("output/cbricks.log",5000);
// 初始化对象池实例,声明对象构造函数
instancePool pool([]()->instance::ptr{
return instance::ptr(new demo);
});
// 信号量,用于作异步逻辑聚合,效果类似于 thread.join()
semaphore sem;
// 并发度
int cnt =10000;
// 并发启动 thread
for(int i =0; i < cnt; i++){
// thread 实例构造
thread thr([&pool,&sem](){
/**
* thread 执行的异步逻辑:
*/
// 1)从对象池中获取对象实例
instance::ptr inst = pool.get();
// 2)将对象实例转为指定类型
std::shared_ptr<demo> d = std::dynamic_pointer_cast<demo>(inst);
// 3)归还对象实例
pool.put(d);
// 4)notify 信号量
sem.notify();
});
}
// 等待所有 thread 执行完成
for(int i =0; i < cnt; i++){
sem.wait();
}
}
最终通过日志输出结果可以看到,实际上只有 50 个左右的 demo 实例被构造和析构,其他操作会复用相同的对象实例,无需反复构造.
4 总结
本篇是我在自学 c++ 过程中针对对象池技术记录的学习实践心得,本文分为两部分内容:
• 第一部分向大家介绍了 go 中对象池 sync.Pool 的使用方式及实现原理
• 第二部分向大家介绍了如何基于 c++ 效仿 sync.Pool 从零实现对象池工具 instancePool
我自学 c++ 过程中所有实践项目都会收录于开源项目 https://github.com/xiaoxuxiansheng/cbricks.git 中,本篇介绍的对象池源码位于 ./pool/instancepool.h 和 ./pool/instancepool.cpp. 大家如有需要,可以自取.