golang sync pool

sync.Pool是内置对象池技术,可用于缓存临时对象,避免因频繁建立临时对象所带来的消耗以及对GC造成的压力

在很多知名框架中都可以看到sync.Pool的大量使用。比如Gin中用sync.Pool来复用每个请求都会创建的gin.Context对象

但是值得注意的是sync.Pool缓存的对象可能被无通知的清理

基本用法

sync.Pool在初始化时,需要用户提供对象构造函数New。用户使用Get来从对象池中获取对象,然后使用Put将对象归还对象池。

type Item struct {A int
}func TestUsePool(t *testing.T) {pool:=sync.Pool{New: func() interface{} {return &Item{A:1,}},}i:=pool.Get().(*Item)fmt.Println(i.A)i1:=&Item{A: 2,}pool.Put(i1)i11:=pool.Get().(*Item)fmt.Println(i11.A)
}

底层实现

以下基于go-1.16

在Golang的GMP调度中,同一时间一个M(系统线程)上只能运行一个P。也就是说,从线程维度来看,在P上的逻辑都是单线程执行的。

sync.Pool就是充分利用了GMP这一特点。对于同一个sync.Pool,每个P都有一个自己的本地对象池poollocal

type pool struct{// 禁止拷贝检测方法noCopy noCopy// 元素类型为poolLocal的数组。储存各个P对应本地对象池local unsafe.Pointer // local fixed-size per-P pool// local数组长度localSize uintptr // size of the local array// 上一轮清理前的对象池victim unsafe.Pointer // local from previous cyclevictimSize uintptr // size of victims array// 创建对象的方法New func() interface{}
}
type poolLocal struct{poolLocalInternalpad	[128-unsafe.SizeOf(poolLocalInternal{}%128)]byte
}type poolLocalInternal struct{// Get、Put操作优先存取private变量private interface{}// p本地对象池shared poolChain
}

poolChain实现

// 池中的双端队列
type poolDequeue struct {// 储存队列的头、尾headTail uint64// 队列元素vals []eface
}// 链节点
type poolChainElt struct {poolDequeuenext, prev *poolChainElt
}// 池链,指向头尾节点
type poolChain struct {head *poolChainElttail *poolChainElt
}

poolChain由图及代码可知是链表+ring buffer的结构。其中采用ringBuffer的理由如下

  • 预先分配内存(可能是为了在put的时候省去内存分配消耗),且分配内存项可不断复用
  • ringBuffer实质上是数组,是连续内存结构,非常利用CPU Cache。在访问poolDequeue某一项,其附近数据项都有可能统一加载到Cache Line,访问速度更快

另一个值得注意的点是head与tail居然并不是独立两个变量。而使用一个64位变量,前32位为head,后32位为tail。

这种打包操作是常见的lock free优化手段

lock free是在多线程情况下访问共有内存时不阻塞彼此的编程手段

对于poolDequeue来说,可能会被多个P同时访问,那么比如在ring buffer仅剩一个时,head-tail==1,同时访问,可能两个P都能获取到对象,而这并不符合预期。

所以采用了CAS操作,是多个P都可能拿到对象,但只有一个P调用CAS成功

Put

Put方法将对象放入池中,按照以下顺序优先放入

  1. 当前P对应的本地缓存池的私有对象
  2. 当前P对应的本地缓存池的共享链表
func (p *pool) Put(x interface{}) {if x == nil {return}// 获取池中当前P对应的本地缓存池l,_ := p.pin()// 优先设置private,若成功,将不会写入shared池if l.private == nil {l.private = xx = nil}// 推入对象到当前P对应的本地缓存池共享链表if x != nil {l.shared.pushHead(x)}runtime_procUnpin()
}
pushHead

pushHead将对象推入链头部

func (c *poolChain) pushHead(val any) {// 若链头节点为空,则初始化d := c.headif d == nil {// Initialize the chain.const initSize = 8 // Must be a power of 2d = new(poolChainElt)d.vals = make([]eface, initSize)c.head = dstorePoolChainElt(&c.tail, d)}// 将元素推入头节点双向队列中if d.pushHead(val) {return}// 若当前双向队列满,则分配两倍于原队列的新队列newSize := len(d.vals) * 2if newSize >= dequeueLimit {// Can't make it any bigger.newSize = dequeueLimit}// 使新队列为链头,并插入对象到新队列中d2 := &poolChainElt{prev: d}d2.vals = make([]eface, newSize)c.head = d2storePoolChainElt(&d.next, d2)d2.pushHead(val)
}

pin

pin方法主要用于

  • 初始化或者重新创建local数组。当local数组为空,或者与当前runtime.GOMAXPROCS不一致,就会触发重新创建local数组以和P数量一致
  • 从当前P中取对应的本地缓存池poolLocal
  • 防止当前P被抢占。
// pin pins the current goroutine to P, disables preemption and
// returns poolLocal pool for the P and the P's id.
// Caller must call runtime_procUnpin() when done with the pool.
func (p *Pool) pin() (*poolLocal, int) {// 获取当前P的id,并禁止抢占pid := runtime_procPin()// 若池的本地缓存池数量大于pid,说明P数量没有变化,可以直接取P所对应的本地缓存池s := runtime_LoadAcquintptr(&p.localSize) // load-acquirel := p.local                              // load-consumeif uintptr(pid) < s {// 为什么是以pid去获取所在local的位置呢return indexLocal(l, pid), pid}return p.pinSlow()
}func indexLocal(l unsafe.Pointer, i int) *poolLocal {lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))return (*poolLocal)(lp)
}
pinSlow
func (p *Pool) pinSlow() (*poolLocal, int) {// 取消P的禁止抢占,使能加上全局池锁runtime_procUnpin()// 全局池加锁后,先再次尝试直接获取allPoolsMu.Lock()defer allPoolsMu.Unlock()pid := runtime_procPin()// poolCleanup won't be called while we are pinned.s := p.localSizel := p.localif uintptr(pid) < s {return indexLocal(l, pid), pid}// 若池的本地池为空,添加到全局中if p.local == nil {allPools = append(allPools, p)}// 新建与当前P数量一致的本地缓存池,并返回当前P的本地缓存池size := runtime.GOMAXPROCS(0)local := make([]poolLocal, size)atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-releaseruntime_StoreReluintptr(&p.localSize, uintptr(size))     // store-releasereturn &local[pid], pid
}
runtime_procPin

runtime_procPin是procPin的封装,主要是为了防止P被抢占以及返回P id

func procPin() int {// 先获取当前goroutine_g_ := getg()// 接着获取goroutine绑定的系统线程,并对该线程加锁// 加锁之后P便不会被抢占,使得不会被GCmp := _g_.mmp.locks++// 返回系统线程绑定的P idreturn int(mp.p.ptr().id)
}

Get

获取对象的顺序如下

  1. 当前P对应的本地缓存池的私有对象
  2. 当前P对应的本地缓存池的共享链表
  3. 其他P对应的本地缓存池的共享链表
  4. 上轮GC幸存缓存池私有对象和共享链表
  5. New方法构造
func (p *pool) Get() interface{} {l, pid := p.pin()// 优先尝试获取当前P对应的本地缓存池的私有对象x := l.privatel.private = nilif x == nil {// 接着尝试当前P对应的本地缓存池的共享链表的头节点x,_ = l.shared.popHead()// 当无法从当前p缓存池获取数据,就会尝试从其他P缓存池获取// 对于其他p的poolChain会调用popTail// 若其他p也没有,那就尝试从victim中取数据if x == nil {x = p.getSlow(pid)}}runtime_procUnpin()// 都获取不到的情况,重新构造if x == nil && p.New != nil {x = p.New()}return x
}

getSlow是先从其他P窃取,然后从victim缓存中获取

func (p *Pool) getSlow(pid int) any {size := runtime_LoadAcquintptr(&p.localSize) // load-acquirelocals := p.local                            // load-consume// 尝试从其他P池窃取对象for i := 0; i < int(size); i++ {l := indexLocal(locals, (pid+i+1)%int(size))// 仅被允许获取其他P的尾部对象if x, _ := l.shared.popTail(); x != nil {return x}}// 若未从其他P窃取到对象,还可以从上轮GC遗留的本地池中获取size = atomic.LoadUintptr(&p.victimSize)if uintptr(pid) >= size {return nil}locals = p.victiml := indexLocal(locals, pid)if x := l.private; x != nil {l.private = nilreturn x}for i := 0; i < int(size); i++ {l := indexLocal(locals, (pid+i)%int(size))if x, _ := l.shared.popTail(); x != nil {return x}}// 获取不到,说明无幸存者atomic.StoreUintptr(&p.victimSize, 0)return nil
}

poolCleanup

poolCleanup在GC之前将pool清空,通过victim将回收拆为了两步,防止GC大量清理导致的抖动

func init() {runtime_registerPoolCleanup(poolCleanup)
}func poolCleanup() {// 清理oldPools上的幸存对象for _, p := range oldPools {p.victim = nilp.victimSize = 0}// 迁移池本地缓存到池victimfor _, p := range allPools {p.victim = p.localp.victimSize = p.localSizep.local = nilp.localSize = 0}// 全局池迁移到oldPoolsoldPools, allPools = allPools, nil
}

Ref

  1. https://www.cyhone.com/articles/think-in-sync-pool/
  2. https://www.cnblogs.com/gaochundong/p/lock_free_programming.html
  3. https://zhuanlan.zhihu.com/p/99710992

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/4531.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图片浏览工具-Honeyview

一、软件特点 轻量而快速 可以显示包括 GPS 信息在内的 JPEG 格式的 EXIF 信息 对图像格式进行批量转换和调整大小 支持显示 GIF 和 WebP 动图 无需解压即可直接查看压缩包中的图像 二、支持的格式 图像格式: BMP, JPG, GIF, PNG, PSD, DDS, JXR, WebP, J2K, JP2, TGA, TIFF, …

基于Pytorch的LSTM网络全流程实验(自带数据集,可直接运行出结果,替换自己的数据集即可使用)

文章目录 LSTM代码双向LSTM&#xff0c;需要修改哪几个参数&#xff1f; LSTM代码 import numpy as np import matplotlib.pyplot as pltimport torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.utils.data import Data…

沉浸式翻译 chrome 插件 Immersive Translate - Translate Website PDF

免费翻译网站&#xff0c;翻译PDF和Epub电子书&#xff0c;双语翻译视频字幕 &#x1f4e3; 网络上口碑爆炸的网站翻译扩展工具【沉浸式翻译】⭐⭐⭐⭐⭐ &#x1f4bb; 功能特点如下&#xff1a; &#x1f4f0; 网站翻译 &#x1f680; 提供双语网站翻译&#xff0c;智能识…

代谢组数据分析五:溯源分析

MetOrigin Analysis {#MetOriginAnalysis} 微生物群及其代谢产物与人类健康和疾病密切相关。然而,理解微生物组和代谢物之间复杂的相互作用是具有挑战性的。 在研究肠道代谢物时,代谢物的来源是一个无法避免的问题即代谢物到底是来自肠道微生物的代谢还是宿主本身代谢产生的…

web自动化系列-selenium的基本方法介绍

web自动化 &#xff0c;一个老生常谈的话题 &#xff0c;很多人的自动化之路就是从它开始 。它学起来简单 &#xff0c;但做起来又比较难以驾驭 &#xff1b;它的执行效率慢 、但又是最接近于用户的操作场景 &#xff1b; 1.web自动化中的三大亮点技术 我们先聊聊 &#xff0…

登录rabbitMQ管理界面时浏览器显示要求进行身份验证,与此站点连接不安全解决办法

问题描述 最近在黑马学习rabbitMQ的过程中&#xff0c;在使用docker部署好rabbitMQ后&#xff0c;使用账号为&#xff1a;itcast&#xff0c;密码为&#xff1a;123321 登录的时候浏览器显示了这个问题&#xff0c;如图所示&#xff1a; 当时以为自己需要输入自己的浏览…

Spring Web MVC入门(3)——响应

目录 一、返回静态页面 RestController 和 Controller之间的关联和区别 二、返回数据ResponseBody ResponseBody作用在类和方法的情况 三、返回HTML代码片段 响应中的Content-Type常见的取值&#xff1a; 四、返回JSON 五、设置状态码 六、设置Header 1、设置Content…

【C++】---STL容器适配器之底层deque浅析

【C】---STL容器适配器之底层deque浅析 一、deque的使用二、deque的原理1、deque的结构2、deque的底层结构&#xff08;1&#xff09;deque的底层空间&#xff08;2&#xff09;deque如何支持随机访问、deque迭代器 3、deque的优缺点&#xff08;1&#xff09;deque的优势&…

java基础之java容器-Collection,Map

java容器 java容器分类一. Collection1. List①. ArrayList② . LinkedList③ . Vector 2. Queue队列①. LinkedList②. PriorityQueue 3. Set集合①. HashSet②. TreeSet 二. Map1. HashMap2.TreeMap3. Hashtable java容器分类 java容器分为两大类&#xff0c;分别是Collecti…

代码随想录算法训练营第五十三天|1143.最长公共子序列 、 1035.不相交的线、 53. 最大子序和

1143 题目&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些…

探索区块链世界:赋能创新,揭示区块链媒体发稿的影响力-世媒讯

区块链&#xff0c;这个由“区块”和“链”组成的概念&#xff0c;可能在您眼中充满神秘和复杂&#xff0c;但其实甚至无所不在&#xff0c;它正静悄悄地改变着我们日常生活的方方面面&#xff0c;从金融到媒体&#xff0c;从医疗到教育。 我们来揭开区块链的神秘面纱。区块链…

VRRP基础

1.基本概念 VRRP(Virtual Router Redundancy protocol,虚拟路由冗余协议&#xff09; VRRP能够在不改变组网的情况下&#xff0c;将多台路由器虚拟成一个虚拟路由器&#xff0c;通过配置虚拟路由器的IP地址为默认网关&#xff0c;实现网关的备份。 VRRP协议版本为VRRPv2&…

SQLServer条件查询,排序

一.常用的运算符 &#xff1a;相等 !&#xff1a;不等 >&#xff1a;大于 <&#xff1a;小于 >&#xff1a;大于等于 <&#xff1a;小于等于 IS NULL&#xff1a;为空 IS NOT NULL&#xff1a;不为空 in&#xff1a;在其中 like&#xff1a;模糊查询 BE…

Java多线程基础

Java多线程 文章目录 Java多线程一、线程介绍及相关概念二、创建和启动线程2.1 Thread类的常用结构2.2 创建线程法1&#xff1a;继承Thread类&#xff08;分配线程对象&#xff09;2.3 创建线程法2&#xff1a;实现Runnable接口&#xff08;创建线程的目标对象&#xff09;2.4 …

揭示C++设计模式中的实现结构及应用——行为型设计模式

简介 行为型模式&#xff08;Behavioral Pattern&#xff09;是对在不同的对象之间划分责任和算法的抽象化。 行为型模式不仅仅关注类和对象的结构&#xff0c;而且重点关注它们之间的相互作用。 通过行为型模式&#xff0c;可以更加清晰地划分类与对象的职责&#xff0c;并…

易错知识点(学习过程中不断记录)

快捷键专区&#xff1a; 注释&#xff1a;ctrl/ ctrlshift/ 保存&#xff1a;ctrls 调试&#xff1a; 知识点专区&#xff1a; 1基本数据类型 基本数据类型有四类&#xff1a;整型、浮点型、字符型、布尔型&#xff08;Boolean&#xff09;&#xff0c; 分为八种&#xff…

JS判断元素是否在数组中

在JavaScript中&#xff0c;有多种方法可以用来判断一个元素是否存在于数组中。以下是其中的一些方法&#xff1a; 1. 使用 Array.prototype.includes() 方法 includes() 方法用于判断一个数组是否包含一个指定的值&#xff0c;根据情况&#xff0c;如果需要区分大小写&#…

AI图书推荐:《企业AI转型:如何在企业中部署ChatGPT?》

Jay R. Enterprise AI in the Cloud. A Practical Guide...ChatGPT Solutions &#xff08;《企业AI转型&#xff1a;如何在企业中部署ChatGPT&#xff1f;》&#xff09;是一本由Rabi Jay撰写、于2024年由John Wiley & Sons出版的书籍&#xff0c;主要为企业提供实施AI转型…

半导体厂FDC系统 的trace data知识

01、什么是FDC系统 在半导体行业中,FDC系统通常指的是"Failure Data Collection"(故障数据收集)系统。FDC系统的作用是收集、存储和分析在半导体制造过程中检测到的故障或不良品数据。以下是FDC系统的一些关键作用: 1. **故障检测**:FDC系统可以实时监测生产线…

python facebook business SDK campaign 广告复制方法

facebook广告复制调试了一天&#xff0c;特此记录&#xff0c;广告复制分为两个步骤&#xff1a; 第一步&#xff1a;使用campaign.create_copy()复制广告系列。 第二步&#xff1a;复制源广告广告集&#xff08;ad_set&#xff09;如果广告集需要修改&#xff0c;使用api_upd…