go/c++ 万字解析对象池技术原理与源码实战

文摘   2024-10-02 19:26   中国台湾  

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 (*poolDequeue) pushHead(val any)bool{
    // ...
}

// 只有当前 P 尝试从 shared 中获取对象实例时,使用此方法
func (*poolDequeue) popHead()(any,bool){
    // ...
}

// 任意 P 可能使用此方法,尝试从当前 P shared 中窃取对象实例
func (*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 (*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 (*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 (*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 (*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 (*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 (*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 (*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 (*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(== 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. 大家如有需要,可以自取.



小徐先生的编程世界
在学钢琴,主业码农
 最新文章