深入浅出Golang中的sync.Pool

目录
  • 一、原理分析
    • 1.1 结构依赖关系图
    • 1.2 用图让代码说话
    • 1.3 Put过程分析
  • 二、学习收获
    • 2.1 如何自己实现一个无锁队列

学习到的内容:

1.一个64位的int类型值,充分利用高32位和低32位,进行相关加减以及从一个64位中拆出高32位和低32位.

扩展:如何自己实现一个无锁队列.

  • 如何判断队列是否满.
  • 如何实现无锁化.
  • 优化方面需要思考的东西.

2.内存相关操作以及优化

  • 内存对齐
  • CPU Cache Line
  • 直接操作内存.

一、原理分析

1.1 结构依赖关系图

下面是相关源代码,不过是已经删减了对本次分析没有用的代码.

type Pool struct {
    // GMP中,每一个P(协程调度器)会有一个数组,数组大小位localSize.
 local     unsafe.Pointer
 // p 数组大小.
 localSize uintptr
 New func() any
}

// poolLocal 每个P(协程调度器)的本地pool.
type poolLocal struct {
 poolLocalInternal
    // 保证一个poolLocal占用一个缓存行
 pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

type poolLocalInternal struct {
 private any       // Can be used only by the respective P. 16
 shared  poolChain // Local P can pushHead/popHead; any P can popTail. 8
}

type poolChain struct {
 head *poolChainElt
 tail *poolChainElt
}

type poolChainElt struct {
 poolDequeue
 next, prev *poolChainElt
}

type poolDequeue struct {
 // head 高32位,tail低32位.
 headTail uint64
 vals []eface
}

// 存储具体的value.
type eface struct {
 typ, val unsafe.Pointer
}

1.2 用图让代码说话

1.3 Put过程分析

Put 过程分析比较重要,因为这里会包含pool所有依赖相关分析.

总的分析学习过程可以分为下面几个步骤:

1.获取P对应的poolLocal

2.val如何进入poolLocal下面的poolDequeue队列中的.

3.如果当前协程获取到当前P对应的poolLocal之后进行put前,协程让出CPU使用权,再次调度过来之后,会发生什么?

4.读写内存优化.

数组直接操作内存,而不经过Golang

充分利用uint64值的特性,将headtail用一个值来进行表示,减少CPU访问内存次数.

获取P对应的poolLocal

sync.Pool.local其实是一个指针,并且通过变量+结构体大小来划分内存空间,从而将这片内存直接划分为数组. Go 在Put之前会先对当前Goroutine绑定到当前P中,然后通过pid获取其在local内存地址中的歧视指针,在获取时是会进行内存分配的. 具体如下:

func (p *Pool) pin() (*poolLocal, int) {
 // 返回运行当前协程的P(协程调度器),并且设置禁止抢占.
 pid := runtime_procPin()
 s := runtime_LoadAcquintptr(&p.localSize) // load-acquire
 l := p.local                              // load-consume
 // pid < 核心数. 默认走该逻辑.
 if uintptr(pid) < s {
  return indexLocal(l, pid), pid
 }
 // 设置的P大于本机CPU核心数.
 return p.pinSlow()
}

// indexLocal 获取当前P的poolLocal指针.
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
 // l p.local指针开始位置.
 // 我猜测这里如果l为空,编译阶段会进行优化.
 lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
 // uintptr真实的指针.
 // unsafe.Pointer Go对指针的封装: 用于指针和结构体互相转化.
 return (*poolLocal)(lp)
}

从上面代码我们可以看到,Go通过runtime_procPin来设置当前Goroutine独占P,并且直接通过头指针+偏移量(数组结构体大小)来进行对内存划分为数组.

Put 进入poolDequeue队列:

Go在Push时,会通过headtail来获取当前队列内元素个数,如果满了,则会重新构建一个环型队列poolChainElt,并且设置为poolChain.head,并且赋值next以及prev.

通过下面代码,我们可以看到,Go通过逻辑运算判断队列是否满的设计时非常巧妙的,如果后续我们去开发组件,也是可以这么进行设计的。

func (c *poolChain) pushHead(val any) {
 d := c.head
    // 初始化.
 if d == nil {
  // Initialize the chain.
  const initSize = 8 // Must be a power of 2
  d = new(poolChainElt)
  d.vals = make([]eface, initSize)
  c.head = d
  // 将新构建的d赋值给tail.
  storePoolChainElt(&c.tail, d)
 }
 // 入队.
 if d.pushHead(val) {
  return
 }
 // 队列满了.
 newSize := len(d.vals) * 2
 if newSize >= dequeueLimit {
        // 队列大小默认为2的30次方.
  newSize = dequeueLimit
 }

    // 赋值链表前后节点关系.
 // prev.
 // d2.prev=d1.
 // d1.next=d2.
 d2 := &poolChainElt{prev: d}
 d2.vals = make([]eface, newSize)
 c.head = d2
 // next .
 storePoolChainElt(&d.next, d2)
 d2.pushHead(val)
}

// 入队poolDequeue
func (d *poolDequeue) pushHead(val any) bool {
 ptrs := atomic.LoadUint64(&d.headTail)
 head, tail := d.unpack(ptrs)
 // head 表示当前有多少元素.
 if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head {
  return false
 }
 // 环型队列. head&uint32(len(d.vals)-1) 表示当前元素落的位置一定在队列上.
 slot := &d.vals[head&uint32(len(d.vals)-1)]

 typ := atomic.LoadPointer(&slot.typ)
 if typ != nil {
  return false
 }

 // The head slot is free, so we own it.
 if val == nil {
  val = dequeueNil(nil)
 }
    // 向slot写入指针类型为*any,并且值为val.
 *(*any)(unsafe.Pointer(slot)) = val
    // headTail高32位++
 atomic.AddUint64(&d.headTail, 1<<dequeueBits)
 return true
}

Get实现逻辑:

其实我们看了Put相关逻辑之后,我们可能很自然的就想到了Get的逻辑,无非就是遍历链表,并且如果队列中最后一个元素不为空,则会将该元素返回,并且将该插槽赋值为空值.

二、学习收获

如何自己实现一个无锁队列. 本文未实现,后续文章会进行实现.

2.1 如何自己实现一个无锁队列

横向思考,并未进行实现,后续会进行实现“

  • 存储直接使用指针来进行存储,充分利用uintptrunsafe.Pointer和结构体指针之间的依赖关系来提升性能.
  • 状态存储要考虑CPU Cache Line、内存对齐以及减少访问内存次数等相关问题.
  • 充分利用Go中的原子操作包来进行实现,通过atomic.CompareAndSwapPointer来设计自旋来达到无锁化.

到此这篇关于深入浅出Golang中的sync.Pool的文章就介绍到这了,更多相关Golang sync.Pool内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Go interface接口声明实现及作用详解

    目录 什么是接口 接口的定义与作用 接口的声明和实现 接口的声明 接口的实现 接口类型断言 空接口 接口实际用途 通过接口实现面向对象多态特性 通过接口实现一个简单的 IoC (Inversion of Control) 什么是接口 接口是一种定义规范,规定了对象应该具有哪些方法,但并不指定这些方法的具体实现.在 Go 语言中,接口是由一组方法签名(方法名.参数类型.返回值类型)定义的.任何实现了这组方法的类型都可以被认为是实现了这个接口. 这种方式使得接口能够描述任意类型的行为,而不用关心其实

  • Golang使用ChatGPT生成单元测试实践

    目录 前言 Part1 easy:单个函数,无复杂依赖 Part2 normal :里面有一些外部import Part3 hard:对外部repo进行mock(gomock举例) 一些痛点 其他用法 前言 目前gpt本质上是续写,所以在待测函数函数定义清晰的情况下,单元测试可以适当依赖它进行生成. 收益是什么: 辅助生成测试用例&测试代码,降低单元测试编写的心智成本 辅助code review,帮助发现代码显式/潜在问题 本文测试环境: gpt: gpt-3.5-turbo go:go 1.1

  • go打包aar及flutter调用aar流程详解

    目录 一.目的 二.背景 三.流程 问题: 问题一:go如何打包为移动端的包 1.环境配置 2.go配置与打包 问题二:flutter如何调用aar 第一步:存放aar与修改gradle配置 第二步:修改MainActivity.java入口代码 第三步:flutter调用 四.结论 一.目的 本篇文章的目的是记录本人使用flutter加载与调用第三方aar包. 二.背景 本人go后端,业余时间喜欢玩玩flutter.一直有一个想法,go可以编译为第三方平台的可执行程序,而flutter可以是一

  • go并发利器sync.Once使用示例详解

    目录 1. 简介 2. 基本使用 2.1 基本定义 2.2 使用方式 2.3 使用例子 3. 原理 4. 使用注意事项 4.1 不能将sync.Once作为函数局部变量 4.2 不能在once.Do中再次调用once.Do 4.3 需要对传入的函数进行错误处理 4.3.1 基本说明 4.3.2 未错误处理导致的问题 4.3.3 处理方式 5. 总结 1. 简介 本文主要介绍 Go 语言中的 Once 并发原语,包括 Once 的基本使用方法.原理和注意事项,从而对 Once 的使用有基本的了解.

  • go sync.Once实现高效单例模式详解

    目录 1. 简介 2. 基本实现 2.1 单例模式定义 2.2 sync.Once实现单例模式 2.3 其他方式实现单例模式 2.3.1 全局变量定义时赋值,实现单例模式 2.3.2 init 函数实现单例模式 2.3.3 使用互斥锁实现单例模式 2.4 使用sync.Once实现单例模式的优点 2.5 sync.Once和init方法适用场景 3. gin中单例模式的使用 3.1 背景 3.2 具体实现 3.3 sync.Once实现单例的好处 4.总结 1. 简介 本文介绍使用sync.On

  • Go CSV包实现结构体和csv内容互转工具详解

    目录 引言 gocsv小档案 gocsv的基本功能 gocsv.UnmarshalFile函数:csv内容转成结构体 gocsv.MarshalFile函数:结构体转成csv文件 自定义类型转换器 自定义CSV的Reader/Writer gocsv包的特点总结 引言 大家在开发中一定遇到过将数据导出成csv格式文件的需求.go标准库中的csv包是只能写入字符串类型的切片.而在go中一般都是将内容写入到结构体中.所以,若使用标准的csv包,就需要将结构体先转换成对应的字符串类型,再写入文件.那可

  • 深入Golang中的sync.Pool详解

    我们通常用golang来构建高并发场景下的应用,但是由于golang内建的GC机制会影响应用的性能,为了减少GC,golang提供了对象重用的机制,也就是sync.Pool对象池. sync.Pool是可伸缩的,并发安全的.其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器. 设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取. 任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩. sync.Pool首先

  • 深度解密 Go 语言中的 sync.Pool

    最近在工作中碰到了 GC 的问题:项目中大量重复地创建许多对象,造成 GC 的工作量巨大,CPU 频繁掉底.准备使用 sync.Pool 来缓存对象,减轻 GC 的消耗.为了用起来更顺畅,我特地研究了一番,形成此文.本文从使用到源码解析,循序渐进,一一道来. 是什么 sync.Pool 是 sync 包下的一个组件,可以作为保存临时取还对象的一个"池子".个人觉得它的名字有一定的误导性,因为 Pool 里装的对象可以被无通知地被回收,可能 sync.Cache 是一个更合适的名字. 有

  • golang中使用sync.Map的方法

    背景 go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map sync.Map思路来自java的ConcurrentHashMap 接口 sync.map就是1.9版本带的线程安全map,主要有如下几种方法: Load(key interface{}) (value interface{}, ok bool) //通过提供一个键key,查找对应的值value,如果不存在,则返回nil.ok的结果表示是否在map中找到值

  • 深入浅出Golang中select的实现原理

    目录 概述 select实现原理 执行流程 case数据结构 执行select 循环 总结 概述 在go语言中,select语句就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case操作,有了select语句,可以实现main主线程与goroutine线程之间的互动.需要的朋友可以参考以下内容,希望对大家有帮助. select实现原理 Golang实现select时,定义了一个数据结构表示每个case语句(包含default,default实际上是一种特殊的case),

  • GoLang中的sync包Once使用执行示例

    目录 背景 One简介 示例 注意 源码解读 背景 在系统初始化的时候,某些代码只想被执行一次,这时应该怎么做呢,没有学习 Once 前,大家可能想到 声明一个标识,表示是否初始化过,然后初始化这个标识加锁,更新这个标识. 但是学会了 One 的使用可以更加简单的解决这个问题 One简介 Once 包主要用于在并发执行代码的时候,某部分代码只会被执行 一次. Once 的使用也非常简单,Once 只有一个 Do 方法,接收一个无参数无返回值的函数类型的参数 f,不管调用多少次 Do 方法,参数

  • 在golang中使用Sync.WaitGroup解决等待的问题

    面对goroutine我们都需要等待它完成交给它的事情,等待它计算完成或是执行完毕,所以不得不在程序需要等待的地方使用time.Sleep()来睡眠一段时间,等待其他goroytine执行完毕,下面的代码打印1到100的for循环可以在很快的时间内运行完毕,但是我们必须添加time.Sleep()来等待其打印完毕,如果我们不等待仿佛什么也没有发生一样.....这肯定不是我们想要的! func main(){ for i := 0; i < 100 ; i++{ go fmt.Println(i)

  • Golang中的sync包的WaitGroup操作

    sync的waitgroup功能 WaitGroup 使用多线程时,进行等待多线程执行完毕后,才可以结束函数,有两个选择 channel waitgroup 首先使用channel func add (n *int , isok chan bool){ for i :=0 ;i <1000 ; i ++ { *n = *n + 1 } isok <- true } func main () { var ok = make(chan bool , 2) var i,u = 0,0 go add(

  • Golang中的sync.WaitGroup用法实例

    WaitGroup的用途:它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成. 官方对它的说明如下: A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and

  • Golang之sync.Pool使用详解

    前言 我们通常用 Golang 来开发并构建高并发场景下的服务,但是由于 Golang 内建的GC机制多少会影响服务的性能,因此,为了减少频繁GC,Golang提供了对象重用的机制,也就是使用sync.Pool构建对象池. sync.Pool介绍 首先sync.Pool是可伸缩的临时对象池,也是并发安全的.其可伸缩的大小会受限于内存的大小,可以理解为是一个存放可重用对象的容器.sync.Pool设计的目的就是用于存放已经分配的但是暂时又不用的对象,而且在需要用到的时候,可以直接从该pool中取.

  • GoLang sync.Pool简介与用法

    目录 使用场景 使用方法 声明对象池 Get & Put 性能测试 使用场景 一句话总结:保存和复用临时对象,减少内存分配,降低GC压力 sync.Pool是可伸缩的,也是并发安全的,其大小仅受限于内存大小.sync.Pool用于存储那些被分配了但是没有使用,而未来可能会使用的值.这样就可以不用再次经过内存分配,可直接复用已有对象,减轻GC的压力,从而提升系统性能. 使用方法 声明对象池 type Student struct { Name string Age int32 Remark [10

随机推荐