go sync pool 快速使用


基本思想

关键思想是对象的复用,避免重复创建、销毁。将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力。

实现原理

sync.Pool 的最底层使用切片加链表来实现双端队列,并将缓存的对象存储在切片中。

type Pool struct {
	noCopy noCopy

    // 每个 P 的本地队列,实际类型为 [P]poolLocal
	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	// [P]poolLocal的大小
	localSize uintptr        // size of the local array

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

	// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数
	New func() interface{}
}

注意事项

  1. Pool 不可以指定⼤⼩,⼤⼩只受制于 GC 临界值。
  2. Pool 里对象的生命周期受 GC 影响,不适合于做连接池,因为连接池需要自己管理对象的生命周期。
  3. 不要对 Get 得到的对象有任何假设,更好的做法是归还对象时,将对象“清空”。

什么时候用

对象池适合在以下场景中使用:

  1. 对象的创建开销较大,如大字符串、大结构体、大切片等。使用对象池可以显著减少对象创建的开销。
  2. 对象的创建频率高,但每个对象的生命周期较短。这种情况下,频繁地创建和销毁对象会导致 GC 压力增大,使用对象池可以缓解这种压力。
  3. 并发量大,多个 goroutine 都需要创建同一类对象。对象池可以减少锁的争用,提高并发性能。
  4. 对象的初始化状态可以抽象为一个函数(sync.PoolNew 函数)。这样可以方便地在对象被获取时重置其状态。

demo

package main

import (
	"bytes"
	"fmt"
	"sync"
	"time"
)

func withoutPool() {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			var buffer bytes.Buffer
			for j := 0; j < 100; j++ {
				buffer.WriteString("Hello, world!")
			}
			_ = buffer.String()
		}()
	}
	wg.Wait()
	elapsed := time.Since(start)
	fmt.Printf("Without pool: %s\n", elapsed)
}

var bufferPool = sync.Pool{
	New: func() interface{} {
		return new(bytes.Buffer)
	},
}

func withPool() {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			buffer := bufferPool.Get().(*bytes.Buffer)
			defer bufferPool.Put(buffer)
			buffer.Reset()
			for j := 0; j < 100; j++ {
				buffer.WriteString("Hello, world!")
			}
			_ = buffer.String()
		}()
	}
	wg.Wait()
	elapsed := time.Since(start)
	fmt.Printf("With pool: %s\n", elapsed)
}
func main() {
	withoutPool()
	withPool()
}

输出结果

Without pool: 1.6958312s
With pool: 433.6141ms

可以看到,性能差别还是很明显的

什么时候不适合用

在以下场景中,使用对象池可能并不合适:

  1. 对象的生命周期需要精确控制。sync.Pool 中的对象随时可能被 GC 回收,无法保证对象的生命周期,因此不适合用于如数据库连接池这样需要长期持有的对象。
  2. 对象的创建和销毁开销并不大,或者对象的创建频率较低。在这种情况下,使用对象池的收益可能无法抵消其引入的复杂性。
  3. 对象的状态需要长期保持。从 sync.Pool 获取的对象状态是不可预知的,如果对象的状态需要在多次使用中保持,那么使用对象池可能会引入 bug。
  4. 代码的可读性和可维护性要求高于性能。使用 sync.Pool 会增加代码的复杂性,降低其可读性,在某些场景下这可能并不是一个好的权衡。

总之,sync.Pool 是一个强大的工具,但并非银弹。在决定是否使用对象池时,需要仔细权衡其收益与成本,根据具体的场景和需求进行选择。


如果本文帮助到了你,帮我点个广告可以咩(o′┏▽┓`o)


评论
  目录