剖析Go1.3新特性:sync.Pool

前端之家收集整理的这篇文章主要介绍了剖析Go1.3新特性:sync.Pool前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

Go 1.3 的sync包中加入一个新特性:Pool。官方文档可以看这里http://golang.org/pkg/sync/#Pool

这个类设计的目的是用来保存和复用临时对象,以减少内存分配,降低CG压力。

  1. type Pool
  2. func (p *Pool) Get() interface{}
  3. func (p *Pool) Put(x interface{})
  4. New func() interface{}

Get返回Pool中的任意一个对象。如果Pool为空,则调用New返回一个新创建的对象。如果没有设置New,则返回nil。

还有一个重要的特性是,放进Pool中的对象,会在说不准什么时候被回收掉。所以如果事先Put进去100个对象,下次Get的时候发现Pool是空也是有可能的。不过这个特性的一个好处就在于不用担心Pool会一直增长,因为Go已经帮你在Pool中做了回收机制。之前我用Channel实现过一个类似接口的Pool,看到这个官方版本之后果断就抛弃了。

下面说说Pool的实现:

1.定时清理

文档上说,保存在Pool中的对象会在没有任何通知的情况下被自动移除掉。实际上,这个清理过程是在每次垃圾回收之前做的。垃圾回收是固定两分钟触发一次。而且每次清理会将Pool中的所有对象都清理掉!(我在看源码之前还以为会按照使用频率清理一部分…)所以如果Pool中的对象数量很多也会拖慢垃圾回收的时间。

  1. var (
  2. allPoolsMu Mutex
  3. allPools []*Pool
  4. )
  5.  
  6. func poolCleanup() {
  7. // This function is called with the world stopped,at the beginning of a garbage collection.
  8. // It must not allocate and probably should not call any runtime functions.
  9. // Defensively zero out everything,2 reasons:
  10. // 1. To prevent false retention of whole Pools.
  11. // 2. If GC happens while a goroutine works with l.shared in Put/Get,// it will retain whole Pool. So next cycle memory consumption would be doubled.
  12. for i,p := range allPools {
  13. allPools[i] = nil
  14. for i := 0; i < int(p.localSize); i++ {
  15. l := indexLocal(p.local,i)
  16. l.private = nil
  17. for j := range l.shared {
  18. l.shared[j] = nil
  19. }
  20. l.a = nil
  21. }
  22. }
  23. allPools = []*Pool{}
  24. }
  25.  
  26. func init() {
  27. runtime_registerPoolCleanup(poolCleanup)
  28. }

有一个全局变量allPools保存了所有被创建出来的Pool对象,并注册了一个poolCleanup函数回调给runtime,这个函数将会在每次垃圾回收之前调用

2.如何管理数据

先看看两个数据结构

  1. type Pool struct {
  2. local unsafe.Pointer // local fixed-size per-P pool,actual type is [P]poolLocal
  3. localSize uintptr // size of the local array
  4.  
  5. // New optionally specifies a function to generate
  6. // a value when Get would otherwise return nil.
  7. // It may not be changed concurrently with calls to Get.
  8. New func() interface{}
  9. }
  10.  
  11. // Local per-P Pool appendix.
  12. type poolLocal struct {
  13. private interface{} // Can be used only by the respective P.
  14. shared []interface{} // Can be used by any P.
  15. Mutex // Protects shared.
  16. pad [128]byte // Prevents false sharing.
  17. }
Pool是提供给外部使用的对象。其中的local成员的真实类型是一个poolLocal数组,localSize是数组长度。poolLocal是真正保存数据的地方。priveate保存了一个临时对象,shared是保存临时对象的数组。

为什么Pool中需要这么多poolLocal对象呢?实际上,Pool是给每个线程分配了一个poolLocal对象。也就是说local数组的长度,就是工作线程的数量(size := runtime.GOMAXPROCS(0))。当多线程在并发读写的时候,通常情况下都是在自己线程的poolLocal中存取数据。当自己线程的poolLocal中没有数据时,才会尝试加锁去其他线程的poolLocal中“偷”数据。

  1. func (p *Pool) Get() interface{} {
  2. if raceenabled {
  3. if p.New != nil {
  4. return p.New()
  5. }
  6. return nil
  7. }
  8. l := p.pin() // 获取当前线程的poolLocal对象,也就是p.local[pid]。
  9. x := l.private
  10. l.private = nil
  11. runtime_procUnpin()
  12. if x != nil {
  13. return x
  14. }
  15. l.Lock()
  16. last := len(l.shared) - 1
  17. if last >= 0 {
  18. x = l.shared[last]
  19. l.shared = l.shared[:last]
  20. }
  21. l.Unlock()
  22. if x != nil {
  23. return x
  24. }
  25. return p.getSlow()
  26. }
Pool.Get的时候,首先会在local数组中获取当前线程对应的poolLocal对象。如果private中有数据,则取出来直接返回。如果没有则先锁住shared,有数据则直接返回。

为什么这里要锁住。答案在getSlow中。因为当shared中没有数据的时候,会尝试去其他的poolLocal的shared中偷数据。

  1. func (p *Pool) getSlow() (x interface{}) {
  2. // See the comment in pin regarding ordering of the loads.
  3. size := atomic.LoadUintptr(&p.localSize) // load-acquire
  4. local := p.local // load-consume
  5. // Try to steal one element from other procs.
  6. pid := runtime_procPin()
  7. runtime_procUnpin()
  8. for i := 0; i < int(size); i++ {
  9. l := indexLocal(local,(pid+i+1)%int(size))
  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. l.Unlock()
  16. break
  17. }
  18. l.Unlock()
  19. }
  20.  
  21. if x == nil && p.New != nil {
  22. x = p.New()
  23. }
  24. return x
  25. }
Go语言的goroutine虽然可以创建很多,但是真正能物理上并发运行的goroutine数量是有限的,是由runtime.GOMAXPROCS(0)设置的。所以这个Pool高效的设计的地方就在于将数据分散在了各个真正并发的线程中,每个线程优先从自己的poolLocal中获取数据,很大程度上降低了锁竞争。

猜你在找的Go相关文章