go学习笔记 Go的sync.Pool源码

本文介绍了Go中的sync.Pool对象池,用于提高性能和减少垃圾回收的影响。sync.Pool提供并发安全的缓存,内部包含本地私有和共享池,避免了锁竞争。文章详细分析了Pool的Get、Put方法以及GC过程,并强调了Pool适用于存储临时、无状态的对象,不适合数据库连接等有状态实例。
摘要由CSDN通过智能技术生成

Pool介绍#

总所周知Go 是一个自动垃圾回收的编程语言,采用三色并发标记算法标记对象并回收。如果你想使用 Go 开发一个高性能的应用程序的话,就必须考虑垃圾回收给性能带来的影响。因为Go 在垃圾回收的时候会有一个STW(stop-the-world,程序暂停)的时间,并且如果对象太多,做标记也需要时间。所以如果采用对象池来创建对象,增加对象的重复利用率,使用的时候就不必在堆上重新创建对象可以节省开销。在Go中,golang提供了对象重用的机制,也就是sync.Pool对象池。 sync.Pool是可伸缩的,并发安全的。其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。 设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。

任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。它对外提供了三个方法:New、Get 和 Put。下面用一个简短的例子来说明一下Pool使用:

package main

import (
	"fmt"
	"sync"
)

var pool *sync.Pool

type Person struct {
	Name string
}

func init() {
	pool = &sync.Pool{
		New: func() interface{} {
			fmt.Println("creating a new person")
			return new(Person)
		},
	}
}

func main() {

	person := pool.Get().(*Person)
	fmt.Println("Get Pool Object1:", person)

	person.Name = "first"
	pool.Put(person)

	fmt.Println("Get Pool Object2:", pool.Get().(*Person))
	fmt.Println("Get Pool Object3:", pool.Get().(*Person))

}

结果:

creating a new person
Get Pool Object1: &{}
Get Pool Object2: &{first}
creating a new person
Get Pool Object3: &{}

这里我用了init方法初始化了一个pool,然后get了三次,put了一次到pool中,如果pool中没有对象,那么会调用New函数创建一个新的对象,否则会从put进去的对象中获取。

存储在池中的任何项目都可以随时自动删除,并且不会被通知。Pool可以安全地同时使用多个goroutine。池的目的是缓存已分配但未使用的对象以供以后重用,从而减轻对gc的压力。也就是说,它可以轻松构建高效,线程安全的free列表。但是,它不适用于所有free列表。池的适当使用是管理一组默认共享的临时项,并且可能由包的并发独立客户端重用。池提供了一种在许多客户端上分摊分配开销的方法。很好地使用池的一个例子是fmt包,它维护一个动态大小的临时输出缓冲区存储。底层存储队列在负载下(当许多goroutine正在积极打印时)进行缩放,并在静止时收缩。另一方面,作为短期对象的一部分维护的空闲列表不适合用于池, 因为在该场景中开销不能很好地摊销。 使这些对象实现自己的空闲列表更有效。首次使用后不得复制池。

pool 的两个特点
1、在本地私有池和本地共享池均获取 obj 失败时,则会从其他p偷一个 obj 返回给调用方。
2、obj在池中的生命周期取决于垃圾回收任务的下一次执行时间,并且从池中获取到的值可能是 put 进去的其中一个值,也可能是 newfun处 新生成的一个值,在应用时很容易入坑。

在多个goroutine之间使用同一个pool做到高效,是因为sync.pool为每个P都分配了一个子池,
当执行一个pool的get或者put操作的时候都会先把当前的goroutine固定到某个P的子池上面,
然后再对该子池进行操作。每个子池里面有一个私有对象和共享列表对象,
私有对象是只有对应的P能够访问,因为一个P同一时间只能执行一个goroutine,
【因此对私有对象存取操作是不需要加锁的】。

源码分析

type Pool struct {
    // 不允许复制,一个结构体,有一个Lock()方法,嵌入别的结构体中,表示不允许复制
	// noCopy对象,拥有一个Lock方法,使得Cond对象在进行go vet扫描的时候,能够被检测到是否被复制
	noCopy noCopy

    //local 和 localSize 维护一个动态 poolLocal 数组
    // 每个固定大小的池, 真实类型是 [P]poolLocal
    // 其实就是一个[P]poolLocal 的指针地址
	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
    // New 是一个回调函数指针,当Get 获取到目标对象为 nil 时,需要调用此处的回调函数用于生成 新的对象
	New func() interface{}
}

Pool结构体里面noCopy代表这个结构体是禁止拷贝的,它可以在我们使用 go vet 工具的时候生效;

local是一个poolLocal数组的指针,localSize代表这个数组的大小;同样victim也是一个poolLocal数组的指针,每次垃圾回收的时候,Pool 会把 victim 中的对象移除,然后把 local 的数据给 victim;local和victim的逻辑我们下面会详细介绍到。

New函数是在创建pool的时候设置的,当pool没有缓存对象的时候,会调用New方法生成一个新的对象。

下面我们对照着pool的结构图往下讲,避免找不到北:

 

// Local per-P Pool appendix.
/*
因为poolLocal中的对象可能会被其他P偷走,
private域保证这个P不会被偷光,至少保留一个对象供自己用。
否则,如果这个P只剩一个对象,被偷走了,
那么当它本身需要对象时又要从别的P偷回来,造成了不必要的开销
*/
type poolLocalInternal struct {
	private interface{} // Can be used only by the respective P.
	shared  poolChain   // Local P can pushHead/popHead; any P can popTail.
}

type poolLocal struct {
	poolLocalInternal

	// Prevents false sharing on widespread platforms with
	// 128 mod (cache line size) = 0 .
    /**
    cache使用中常见的一个问题是false sharing。
    当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。
    false sharing会导致多核处理器上严重的系统性能下降。
    字节对齐,避免 false sharing (伪共享)
    */
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

local字段存储的是一个poolLocal数组的指针,poolLocal数组大小是goroutine中P的数量,访问时,P的id对应poolLocal数组下标索引,所以Pool的最大个数runtime.GOMAXPROCS(0)。

通过这样的设计,每个P都有了自己的本地空间,多个 goroutine 使用同一个 Pool 时,减少了竞争,提升了性能。如果对goroutine的P、G、M有疑惑的同学不妨看看这篇文章:The Go scheduler

poolLocal里面有一个pad数组用来占位用,防止在 cache line 上分配多个 poolLocalInternal从而造成false sharing,cache使用中常见的一个问题是false sharing。当不同的线程同时读写同一cache line上不同数据时就可能发生false sharing。false sharing会导致多核处理器上严重的系统性能下降。具体的可以参考伪共享(False Sharing)

poolLocalInternal包含两个字段private和shared。

private代表缓存的一个元素,只能由相应的一个 P 存取。因为一个 P 同时只能执行一个 goroutine,所以不会有并发的问题;所以无需加锁

shared则可以由任意的 P 访问,但是只有本地的 P 才能 pushHead/popHead,其它 P 可以 popTail。因为可能有多个goroutine同时操作,所以需要加锁。

type poolChain struct {
	// head is the poolDequeue 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值