golang sync.Pool

临时对象池

  当多个goroutine都需要创建同一个对象的时候,如果goroutine过多,可能导致对象的创建数目剧增。而对象又是占用内存的,进而导致的就是内存回收的GC压力徒增。造成“并发大-占用内存大-GC缓慢-处理并发能力降低-并发更大”这样的恶性循环。这时候,我们非常迫切需要有一个对象池,每个gotoutine不再自己单独创建对象,而是从对象池中获取出一个对象(如果池中已经有的话)。这就是sync.Pool出现的目的了

  看下面这个例子(取自gomemcache):

// keyBufPool returns []byte buffers for use by PickServer's call to
// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
// copies, which at least are bounded in size and small)
var keyBufPool = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 256)
        return &b
    },
}

func (ss *ServerList) PickServer(key string) (net.Addr, error) {
    ss.mu.RLock()
    defer ss.mu.RUnlock()
    if len(ss.addrs) == 0 {
        return nil, ErrNoServers
    }
    if len(ss.addrs) == 1 {
        return ss.addrs[0], nil
    }
    bufp := keyBufPool.Get().(*[]byte)
    n := copy(*bufp, key)
    cs := crc32.ChecksumIEEE((*bufp)[:n])
    keyBufPool.Put(bufp)

    return ss.addrs[cs%uint32(len(ss.addrs))], nil
}

  这是实际项目中的一个例子,这里使用keyBufPool的目的是为了让crc32.ChecksumIEEE所使用的[]bytes数组可以重复使用,减少GC的压力。

  但是这里可能会有一个问题,我们没有看到Pool的手动回收函数。 那么是不是就意味着,如果我们的并发量不断增加,这个Pool的体积会不断变大,或者一直维持在很大的范围内呢?

  答案是不会的,sync.Pool的回收是有的,它是在系统自动GC的时候,触发pool.go中的poolCleanup函数。

func poolCleanup() {
    for i, p := range allPools {
        allPools[i] = nil
        for i := 0; i < int(p.localSize); i++ {
            l := indexLocal(p.local, i)
            l.private = nil
            for j := range l.shared {
                l.shared[j] = nil
            }
            l.shared = nil
        }
        p.local = nil
        p.localSize = 0
    }
    allPools = []*Pool{}
}

  这个函数会把Pool中所有goroutine创建的对象都进行销毁。

  那这里另外一个问题也凸显出来了,很可能我上一步刚往pool中PUT一个对象之后,下一步GC触发,导致pool的GET函数获取不到PUT进去的对象。 这个时候,GET函数就会调用New函数,临时创建出一个对象,并存放到pool中。

  根据以上结论,sync.Pool其实不适合用来做持久保存的对象池(比如连接池)。它更适合用来做临时对象池,目的是为了降低GC的压力。

  连接池性能测试

package main

import (
    "sync"
    "testing"
)

var bytePool = sync.Pool{
    New: newPool,
}

func newPool() interface{} {
    b := make([]byte, 1024)
    return &b
}
func BenchmarkAlloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        obj := make([]byte, 1024)
        _ = obj
    }
}

func BenchmarkPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        obj := bytePool.Get().(*[]byte)
        _ = obj
        bytePool.Put(obj)
    }
}

  文件目录下执行 go test -bench .

E:\MyGo\sync>go test -bench .
testing: warning: no tests to run
PASS
BenchmarkAlloc-4        50000000                39.3 ns/op
BenchmarkPool-4         50000000                25.4 ns/op
ok      _/E_/MyGo/sync  3.345s

  通过性能测试可以清楚地看到,使用连接池消耗的CPU时间远远小于每次手动分配内存。

你可能感兴趣的:(golang sync.Pool)