jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

深入Golang之sync.pool

作者:jasper | 分类:Golang | 标签:   | 阅读 83 次 | 发布:2017-11-25 10:12 p.m.

在高并发或者大量的数据请求的场景中,我们会遇到很多问题,垃圾回收就是其中之一(garbage collection),为了减少优化GC,我们一般想到的方法就是能够让对象得以重用。这就需要一个对象池来存储待回收对象,等待下次重用,从而减少对象产生数量。

和其他语言一样,在Golang中也有原生的对象池来达到这一的目的,这就是sync里面的pool的使用。

数据结构

type Pool struct {
    noCopy noCopy

    local     unsafe.Pointer
    localSize uintptr       
    New func() interface{}
}

Pool在Golang是全局唯一的,不能被复制,用noCopy来控制,具体的限制方式为:

uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
        !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
        uintptr(*c) != uintptr(unsafe.Pointer(c))

其中New是一个function,但是是可选的,如果设置了,那么在Get的时候如果为nil就会用New来生成。local是真正存储对象的,它的类型是poolLocal:

type poolLocal struct {
    poolLocalInternal

    pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

type poolLocalInternal struct {
    private interface{}  
    shared  []interface{}
    Mutex                
}

poolLocal数组的大小即P的数量,也就是给每一个系统线程分配了一个poolLocal,而poolLocal类型中保存了真正的数据。poolLocal分为private和shared两个域来保存对象,因为poolLocal中的对象可能会被其他P偷走,private域保证这个P不会被偷光,至少保留一个对象供自己用。否则,如果这个P只剩一个对象,被偷走了,那么当它本身需要对象时又要从别的P偷回来,造成了不必要的开销。

Put

先来看看将一个对象放入到pool中:

func (p *Pool) Put(x interface{}) {
    l := p.pin()
    if l.private == nil {
        l.private = x
        x = nil
    }
    runtime_procUnpin()
    if x != nil {
        l.Lock()
        l.shared = append(l.shared, x)
        l.Unlock()
    }
}

首先会试图将新对象放到自己的poolLocal.private中,如果private已经有对象了,就会放到shared的尾部。

不过我们可以来多看看怎么拿到当前线程的poolLocal的,方法是pin():

func (p *Pool) pin() *poolLocal {
    pid := runtime_procPin()
    s := atomic.LoadUintptr(&p.localSize)
    l := p.local                         
    if uintptr(pid) < s {
        return indexLocal(l, pid)
    }
    return p.pinSlow()
}

func procPin() int {
    _g_ := getg()
    mp := _g_.m

    mp.locks++
    return int(mp.p.ptr().id)
}

具体的逻辑就是首先拿到当前的pid,然后以pid作为index找到local中的poolLocal,但是如果pid大于了localsize,说明当前线程的poollocal不存在,就会新创建一个poolLocal:

    local := make([]poolLocal, size)
    atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) 
    atomic.StoreUintptr(&p.localSize, uintptr(size))        
    return &local[pid]

Get

我们再来看看如何从pool中获取一个对象:

func (p *Pool) Get() interface{} {
   l := p.pin()
    x := l.private
    l.private = nil
    runtime_procUnpin()
    if x == nil {
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
        }
        l.Unlock()
        if x == nil {
            x = p.getSlow()
        }
    }
        if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}   

同样是先获取poolLocal,这个和上面说的逻辑一样的,然后首先从private中拿,如果没有就从shared的尾部来拿,如果仍然没有,就从其他P的poollocal的shared来拿,如果还是没有,就调用New()来生成。

这里从其他P来偷的实现为getSlow:

func (p *Pool) getSlow() (x interface{}) {
    size := atomic.LoadUintptr(&p.localSize) 
    local := p.local  
    pid := runtime_procPin()
    runtime_procUnpin()
    for i := 0; i < int(size); i++ {
        l := indexLocal(local, (pid+i+1)%int(size))
        l.Lock()
        last := len(l.shared) - 1
        if last >= 0 {
            x = l.shared[last]
            l.shared = l.shared[:last]
            l.Unlock()
            break
        }
        l.Unlock()
    }
    return x
}

具体逻辑就是从pid的下一个index开始,挨个遍历其他P的poolLocal,一旦发现shared数组不为空,就将尾部的对象偷走并返回。

Cleanup

首先我们来看看pool里面的对象什么时候会被清除,在初始化的时候,就会把pool的清除函数注册到runtime中:

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

具体什么时候调用这个poolCleanup方法呢?顺着代码我们找到了调用的地方:

func gcStart(mode gcMode, trigger gcTrigger) {
    // ......
    clearpools()
    // ......
}

是的,就是在GC之前,确切地说是在STW的时候,这时候可以保证不会有任何的runtime function来调用。再来看看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{}
}

简单粗暴,就是把所有的poollocal里面的private和shared都置为空。这样原本Pool中储存的对象会被GC全部回收。

是的,这就是Golang中的对象池和其他语言的最大的不同,它们是会被GC掉的,这样就让这样的pool有自己独特的用途。首先,有状态的对象绝不能储存在Pool中,Pool不能用作连接池(类似数据库连接池等)。其次,你不需要担心Pool会一直增长,因为runtime定期帮你回收Pool中的数据。但是也不能无限制地向Pool中Put新的对象,这样会拖累GC。

结论

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

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

转载请注明出处:http://www.opscoder.info/golang_pool.html

【上一篇】 深入Golang之sync.mutex
【下一篇】 深入Golang之netpoller
其他分类: