当前位置: 首页 > news >正文

从golang的sync.pool到linux的slab分配器

最近学习golang的时候,看到golang并发编程中有一个sync.pool,即对象池,猛地一看这不跟linux的slab分配器类似嘛,赶紧学习记录下

这里先总结下设计sync.pool和slab的目的

  • sync.pool
    • 为了缓解特定类型的对象频繁创建和销毁,导致gc压力大的问题
  • slab
    • linux使用伙伴系统管理内存,伙伴系统分配内存时以页Page(4K byte)为单位进行分配,而linux内核中,会频繁的创建struct page, struct fd,struct task的实例,这些结构体对象的size通常只有几十个字节,如果每次都从伙伴系统分配内存,将造成极大的内存浪费

sync.pool

参考了:

基本用法:https://geektutu.com/post/hpg-sync-pool.html

基本用法+源码分析:https://www.cnblogs.com/qcrao-2018/p/12736031.html

底层分析更全面:https://www.cyhone.com/articles/think-in-sync-pool/

sync.pool的特点

并发安全并且lock-free,sync.pool之所以能做到lock-free,跟golang的GMP调度模型有关系,下面再进行详细介绍

sync.pool的使用场景

当存在对象被频繁的重复分配时,可以使用sync.pool避免对象重复创建和销毁,减轻gc的压力

sync.pool的基本用法

创建对象池

var studentPool = sync.Pool{New: func() interface{} { return new(Student) },
}

理解:创建一个sync.pool对象,命名为studentPool,创建sync.pool对象时,传入的New成员是一个函数,该函数定义了创建student对象的行为。需要注意的是,sync.pool在初始化时只会创建一个对象,后续调用Get发现没有对象时,才会继续创建对象

向对象池添加/获取一个对象

stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)

完整的例子

package mainimport ("fmt""sync"
)var pool *sync.Pooltype Person struct {Name string
}func initPool() {pool = &sync.Pool{New: func() interface{} {fmt.Println("Creating a new Person")return new(Person)},}
}func main() {initPool()p := pool.Get().(*Person)fmt.Println("首次从 pool 里获取:", p)p.Name = "first"fmt.Printf("设置 p.Name = %s\n", p.Name)pool.Put(p)fmt.Println("p.name:", p.Name) // p仍然有效p1 := pool.Get().(*Person)fmt.Println("p1.name:", p1.Name)fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}

输出

Creating a new Person
首次从 pool 里获取: &{}
设置 p.Name = first
p.name: first
p1.name: first
Creating a new Person
Pool 没有对象了,调用 Get:  &{}

从这个例子可以看出,当p被存入pool后,p仍然是有效的,并且p的字段并没有因为p存入pool而被清空

sync.pool底层实现

sync.pool结构体

type Pool struct {noCopy noCopy// 每个 P 的本地队列,实际类型为 [P]poolLocallocal     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal// [P]poolLocal的大小localSize uintptr        // size of the local arrayvictim     unsafe.Pointer // local from previous cyclevictimSize uintptr        // size of victims array// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数New func() interface{}
}

上文提到,sync.pool是lock-free的,关键就在于这里的local是一个[GOMAXPROCS]poolLocal数组,由于每个 P 都有自己的一个本地对象池 poolLocal,Get 和 Put 操作都会优先存取本地对象池。由于 P 的特性,操作本地对象池的时候整个并发问题就简化了很多,可以尽量避免并发冲突

poolLocal定义如下

type poolLocal struct {poolLocalInternal// 将 poolLocal 补齐至两个缓存行的倍数,防止 false sharing,// 每个缓存行具有 64 bytes,即 512 bit// 目前我们的处理器一般拥有 32 * 1024 / 64 = 512 条缓存行// 伪共享,仅占位用,防止在 cache line 上分配多个 poolLocalInternalpad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}// Local per-P Pool appendix.
type poolLocalInternal struct {// P 的私有缓存区,使用时无需要加锁private interface{}// 公共缓存区。本地 P 可以 pushHead/popHead;其他 P 则只能 popTailshared  poolChain
}

poolChain 是一个双端队列的实现

type poolChain struct {// 只有生产者会 push to,不用加锁(Get不会触发对象窃取)head *poolChainElt// 读写需要原子控制。 pop fromtail *poolChainElt
}type poolChainElt struct {poolDequeue// next 被 producer 写,consumer 读。所以只会从 nil 变成 non-nil// prev 被 consumer 写,producer 读。所以只会从 non-nil 变成 nilnext, prev *poolChainElt
}type poolDequeue struct {// The head index is stored in the most-significant bits so// that we can atomically add to it and the overflow is// harmless.// headTail 包含一个 32 位的 head 和一个 32 位的 tail 指针。这两个值都和 len(vals)-1 取模过。// tail 是队列中最老的数据,head 指向下一个将要填充的 slot// slots 的有效范围是 [tail, head),由 consumers 持有。headTail uint64// vals 是一个存储 interface{} 的环形队列,它的 size 必须是 2 的幂// 如果 slot 为空,则 vals[i].typ 为空;否则,非空。// 一个 slot 在这时宣告无效:tail 不指向它了,vals[i].typ 为 nil// 由 consumer 设置成 nil,由 producer 读vals []eface
}

sync.pool Get接口调用时,可能会触发对象窃取,poolDeque可能被多个P访问,sync.pool内部使用CAS原子操作保证并发安全性

slab分配器

参考:https://segmentfault.com/a/1190000043626203

为什么需要slab分配器

linux伙伴系统管理内存的最小单位是物理内存页page,如果为几十个或者几百个字节的分配请求分配一整个页,将造成大量内存

slab分配器在内核代码中使用

对于用户空间,glibc的malloc内部维护一个空闲链表

使用slab分配器的好处

缓存友好

利用 CPU 高速缓存提高访问速度。当一个对象被直接释放回 slab 对象池中的时候,这个内核对象还是“热的”,仍然会驻留在 CPU 高速缓存中。如果这时,内核继续向 slab 对象池申请对象,slab 对象池会优先把这个刚刚释放 “热的” 对象分配给内核使用,因为对象很大概率仍然驻留在 CPU 高速缓存中,所以内核访问起来速度会更快

弥补伙伴系统调用路径长和只能按照整个物理页分配的缺点

伙伴系统只能分配 2 的次幂个完整的物理内存页,这会引起占用高速缓存以及 TLB 的空间较大,导致一些不重要的数据驻留在 CPU 高速缓存中占用宝贵的缓存空间,而重要的数据却被置换到内存中。 slab 对象池针对小内存分配场景,可以有效的避免这一点

slab分配器使用场景

主要是分配如下结构体对象的场景

  • struct task_struct

  • struct mm_struct

  • struct fd

  • struct page

  • struct socket

对于每种结构体,内核会创建一个专属的slab分配器

slab底层原理

暂时只需要了解slab是什么,和slab的基本概念。前面的区域,后面再探索吧~

相关文章:

  • python中从队列里取出全部元素的两种写法
  • vue注册自定义指令
  • CSS 预处理器与工具
  • MCP 技术完全指南:微软开源项目助力 AI 开发标准化学习
  • PostgreSQL 的扩展pageinspect
  • github中main与master,master无法合并到main
  • 408第一季 - 数据结构 - 树与二叉树II
  • Python实例题:Python计算微积分
  • C++ 中的编译期计算(Compile-Time Computation)
  • Nature子刊:16S宏基因组+代谢组学联动,借助MicrobiomeGS2建模揭示IBD代谢治疗新靶点
  • 《经济学原理》第9版第6章供给、需求和政府政策
  • 历史数据分析——唐山港
  • 探索NoSQL注入的奥秘:如何消除MongoDB查询中的前置与后置条件
  • Unity | AmplifyShaderEditor插件基础(第五集:简易膨胀shader)
  • Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
  • 向 AI Search 迈进,腾讯云 ES 自研 v-pack 向量增强插件揭秘
  • 【基础算法】差分算法详解
  • 在 Windows 11 或 10 上将 Visual Studio Code 添加到系统路径
  • 永恒之蓝(CVE-2017-0146)详细复现
  • 每日Prompt:治愈动漫插画
  • 网站开发ceac证/网站流量统计分析工具
  • 上海网站开发caiyiduo/百度手机助手官网下载
  • 怎么用linux做网站服务器吗/梁水才seo优化专家
  • 百度网站前三名权重一般在多少/自己建网站需要钱吗
  • 乐清市网站建设/上海搜索引擎优化公司排名
  • bootstrop新闻网站开发/长春seo