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

任何存放区其中的值可以在任何时候被删除而不通知,在高负载下可以动态的扩容,在不活跃时对象池会收缩。

sync.Pool 首先声明了两个结构体

  1. type poolLocalInternal struct {
  2. private interface{}
  3. shared []interface{}
  4. Mutex
  5. }
  6. type poolLocal struct {
  7. poolLocalInternal
  8. pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
  9. }

为了使得在多个 goroutine 中高效的使用 goroutine,sync.Pool 为每个 P(对应 CPU) 都分配一个本地池,当执行 Get 或者 Put 操作的时候,会先将 goroutine 和某个 P 的子池关联,再对该子池进行操作。 每个 P 的子池分为私有对象和共享列表对象,私有对象只能被特定的 P 访问,共享列表对象可以被任何 P 访问。因为同一时刻一个 P 只能执行一个 goroutine,所以无需加锁,但是对共享列表对象进行操作时,因为可能有多个 goroutine 同时操作,所以需要加锁。

值得注意的是 poolLocal 结构体中有个 pad 成员,目的是为了防止 false sharing。cache 使用中常见的一个问题是 false sharing。当不同的线程同时读写同一 cache line 上不同数据时就可能发生 false sharing。false sharing 会导致多核处理器上严重的系统性能下降。具体的可以参考伪共享 (False Sharing)

类型 sync.Pool 有两个公开的方法,一个是 Get,一个是 Put, 我们先来看一下 Put 的源码。

  1. func (p *Pool) Put(x interface{}) {
  2. if x == nil {
  3. return
  4. }
  5. if race.Enabled {
  6. if fastrand()%4 == 0 {
  7. return
  8. }
  9. race.ReleaseMerge(poolRaceAddr(x))
  10. race.Disable()
  11. }
  12. l := p.pin()
  13. if l.private == nil {
  14. l.private = x
  15. x = nil
  16. }
  17. runtime_procUnpin()
  18. if x != nil {
  19. l.Lock()
  20. l.shared = append(l.shared, x)
  21. l.Unlock()
  22. }
  23. if race.Enabled {
  24. race.Enable()
  25. }
  26. }
  1. 如果放入的值为空,直接 return.
  2. 检查当前 goroutine 的是否设置对象池私有值,如果没有则将 x 赋值给其私有成员,并将 x 设置为 nil。
  3. 如果当前 goroutine 私有值已经被设置,那么将该值追加到共享列表。
  1. func (p *Pool) Get() interface{} {
  2. if race.Enabled {
  3. race.Disable()
  4. }
  5. l := p.pin()
  6. x := l.private
  7. l.private = nil
  8. runtime_procUnpin()
  9. if x == nil {
  10. l.Lock()
  11. last := len(l.shared) - 1
  12. if last >= 0 {
  13. x = l.shared[last]
  14. l.shared = l.shared[:last]
  15. }
  16. l.Unlock()
  17. if x == nil {
  18. x = p.getSlow()
  19. }
  20. }
  21. if race.Enabled {
  22. race.Enable()
  23. if x != nil {
  24. race.Acquire(poolRaceAddr(x))
  25. }
  26. }
  27. if x == nil && p.New != nil {
  28. x = p.New()
  29. }
  30. return x
  31. }
  1. 尝试从本地 P 对应的那个本地池中获取一个对象值, 并从本地池冲删除该值。
  2. 如果获取失败,那么从共享池中获取, 并从共享队列中删除该值。
  3. 如果获取失败,那么从其他 P 的共享池中偷一个过来,并删除共享池中的该值 (p.getSlow())。
  4. 如果仍然失败,那么直接通过 New() 分配一个返回值,注意这个分配的值不会被放入池中。New() 返回用户注册的 New 函数的值,如果用户未注册 New,那么返回 nil。

深入Golang之sync.Pool详解 - sunsky303 - 博客园 - 图1

最后我们来看一下 init 函数。

  1. func init() {
  2. runtime_registerPoolCleanup(poolCleanup)
  3. }

可以看到在 init 的时候注册了一个 PoolCleanup 函数,他会清除掉 sync.Pool 中的所有的缓存的对象,这个注册函数会在每次 GC 的时候运行,所以 sync.Pool 中的值只在两次 GC 中间的时段有效。

深入Golang之sync.Pool详解 - sunsky303 - 博客园 - 图2

package main

import ( “sync”
“time”
“fmt” ) var bytePool = sync.Pool{New: func() interface{} {
b :\= make([]byte, 1024) return &b
},
}

func main() { //defer //debug.SetGCPercent(debug.SetGCPercent(-1))
a := time.Now().Unix() for i:=0;i<1000000000;i++{
obj :\= make([]byte, 1024)
\= obj
}
b :\= time.Now().Unix() for j:=0;j<1000000000;j++ {
obj :\= bytePool.Get().(*[]byte)
\= obj
bytePool.Put(obj)
}

  1. c :\= time.Now().Unix()
  2. fmt.Println("without pool ", b - a, "s")
  3. fmt.Println("with pool ", c - b, "s")

}

深入Golang之sync.Pool详解 - sunsky303 - 博客园 - 图3

深入Golang之sync.Pool详解 - sunsky303 - 博客园 - 图4

深入Golang之sync.Pool详解 - sunsky303 - 博客园 - 图5

可见 GC 对性能影响不大,因为 shared list 太长也会耗时。

总结:

通过以上的解读,我们可以看到,Get 方法并不会对获取到的对象值做任何的保证,因为放入本地池中的值有可能会在任何时候被删除,但是不通知调用者。放入共享池中的值有可能被其他的 goroutine 偷走。 所以对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池重的值有可能会在垃圾回收时被删除掉,这违反了数据库连接池建立的初衷。

根据上面的说法,Golang 的对象池严格意义上来说是一个临时的对象池,适用于储存一些会在 goroutine 间分享的临时对象。主要作用是减少 GC,提高性能。在 Golang 中最常见的使用场景是 fmt 包中的输出缓冲区。

在 Golang 中如果要实现连接池的效果,可以用 container/list 来实现,开源界也有一些现成的实现,比如go-commons-pool,具体的读者可以去自行了解。

参考资料: