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 cycle
  victimSize uintptr // size of victims array
  
  // 创建对象的方法
  New func() interface{}
}
type poolLocal struct{
  poolLocalInternal
  pad	[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 {
	poolDequeue
	next, prev *poolChainElt
}

// 池链,指向头尾节点
type poolChain struct {
	head *poolChainElt
	tail *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 = x
		x = nil
	}

	// 推入对象到当前P对应的本地缓存池共享链表
	if x != nil {
		l.shared.pushHead(x)
	}

	runtime_procUnpin()
}
pushHead

pushHead将对象推入链头部

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
		storePoolChainElt(&c.tail, d)
	}

  // 将元素推入头节点双向队列中
	if d.pushHead(val) {
		return
	}

  // 若当前双向队列满,则分配两倍于原队列的新队列
	newSize := len(d.vals) * 2
	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)
}

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-acquire
	l := p.local                              // load-consume
	if 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.localSize
	l := p.local
	if 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-release
	runtime_StoreReluintptr(&p.localSize, uintptr(size))     // store-release
	return &local[pid], pid
}

runtime_procPin

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

func procPin() int {
  // 先获取当前goroutine
	_g_ := getg()
  
  // 接着获取goroutine绑定的系统线程,并对该线程加锁
  // 加锁之后P便不会被抢占,使得不会被GC
	mp := _g_.m
	mp.locks++
  
  // 返回系统线程绑定的P id
	return 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.private
	l.private = nil

	if 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-acquire
	locals := 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.victim
	l := indexLocal(locals, pid)
	if x := l.private; x != nil {
		l.private = nil
		return 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 = nil
		p.victimSize = 0
	}

	// 迁移池本地缓存到池victim
	for _, p := range allPools {
		p.victim = p.local
		p.victimSize = p.localSize
		p.local = nil
		p.localSize = 0
	}

	// 全局池迁移到oldPools
	oldPools, 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
  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Golang 的内存池使用起来很简单,只需要使用 sync.Pool 包中的 Get 和 Put 方法,就可以在内存池中请求和释放内存块: // 创建一个内存池 pool := &sync.Pool { New: func() interface{} { b := make([]byte, 1024) return &b }, }// 从内存池中请求内存块 buf := pool.Get().(*[]byte)// 使用完毕后,将内存块释放回内存池 pool.Put(buf) ### 回答2: 使用 golang 的内存池可提高性能,减少内存分配的开销,并减轻垃圾回收的负担。下面是一个简单的示例代码: 首先,我们需要导入 "sync" 包用于同步操作,以及 "github.com/oleiade/lane" 包用于实现队列(FIFO)。 ``` import ( "sync" "github.com/oleiade/lane" ) // 定义一个内存池结构 type MemoryPool struct { pool *lane.Queue // 使用队列实现内存池 maxSize int // 内存池的最大容量 lock sync.Mutex // 用于同步操作的锁 } // 初始化内存池 func NewMemoryPool(maxSize int) *MemoryPool { return &MemoryPool{ pool: lane.NewQueue(), // 初始化队列 maxSize: maxSize, // 设置最大容量 } } // 获取一个对象 func (p *MemoryPool) Get() interface{} { p.lock.Lock() // 加锁 defer p.lock.Unlock() if p.pool.Size() > 0 { return p.pool.Dequeue() // 如果池中有可用对象,则出队并返回 } return nil // 池中无可用对象时返回 nil } // 释放一个对象 func (p *MemoryPool) Put(obj interface{}) { p.lock.Lock() // 加锁 defer p.lock.Unlock() if p.pool.Size() < p.maxSize { p.pool.Enqueue(obj) // 如果池未满,则将对象入队 } } ``` 这份代码中,我们创建了一个内存池结构 `MemoryPool`,其中包含一个 `lane.Queue` 类型的队列 `pool`,用于存储已经分配的对象。`maxSize` 属性表示内存池的最大容量。 在 `Get()` 方法中,首先通过加锁操作来保证并发安全。如果内存池中有可用对象,就将对象从队列中出队并返回;否则返回 `nil`。 在 `Put()` 方法中,也通过加锁来保证并发安全。如果内存池未满,就将对象入队。 这样,我们就实现了一个简单的 golang 内存池。在实际应用中,可以根据具体需求对内存池进行优化和扩展。 ### 回答3: Golang内存池是一种可以提供重复使用的内存资源的机制,可以有效地减少内存分配和回收的开销。以下是一个简单的使用Golang内存池的代码示例: ```go package main import ( "fmt" "sync" ) var pool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) // 初始化一个长度为1024的字节切片 }, } func main() { // 从内存池中获取一个字节切片 data := pool.Get().([]byte) // 断言类型为[]byte // 使用字节切片进行数据操作 copy(data, []byte("Hello, world!")) // 打印字节切片中的数据 fmt.Println(string(data)) // 将字节切片重新放回内存池 pool.Put(data) // 从内存池中获取一个新的字节切片 newData := pool.Get().([]byte) // 此时newData的内容与之前的data相同,可以继续使用 fmt.Println(string(newData)) // 清空内存池 pool.New = nil } ``` 在以上示例中,我们首先创建了一个sync.Pool对象,其New字段是一个匿名函数,用于创建一个长度为1024的字节切片,并将其作为默认的新对象。 接下来,在main函数中,我们通过pool.Get()方法从内存池中获取一个字节切片,并进行数据操作。当我们完成后,通过pool.Put()方法将字节切片重新放回内存池。 对于下一次需要新的字节切片时,我们可以再次调用pool.Get()方法,此时会从内存池中获取之前放回的字节切片,因此可以继续使用。 最后,我们可以通过将pool.New字段设置为nil来清空内存池,使其在下一次pool.Get()时重新生成新的对象。 使用内存池可以避免频繁的内存分配和回收操作,提高程序的性能。不过需要注意的是,内存池中的对象并不是永久存在的,可能会在不同的时间点被回收,因此必要时需要考虑重新分配内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值