Golang:sync.Pool

临时对象池

  • sync.Pool类型可以被称为临时对象池子,它的值可以被用来存储临时的对象。它的值在被真正使用后,就不应该再复制了
  • 临时对象的意思是:不需要持久使用的某一类值,这类值对于程序来说可有可无,但如果有的话明显更好。它们的创建和销毁可以在任何时候发生,不影响程序功能。临时对象应该是无需被区分的,其中任何一个值都可以代替另一个。
  • 临时对象池最主要的用途就是充当数据缓存。
  • sync.Pool类型有两个外部方法,Put和Get,Put用于在当前的池子中存放临时对象,它接收一个interface{}类型参数;Get用于从当前的池子中获取临时对象,它返回一个interface{}类型的值(实际上是从池子中删除任意一个值并将其返回,如果池子是空的,就用Pool.New创建并直接返回,创建的值不会存入池子中)。
  • New字段的修改不应该和Get方法同时调用,如果New字段是nil,而池子里也没有数据,那么Get方法最终会返回nil
type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() interface{}
}

...
...
...

// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
	if x == nil {
		return
	}
	if race.Enabled {
		if fastrand()%4 == 0 {
			// Randomly drop x on floor.
			return
		}
		race.ReleaseMerge(poolRaceAddr(x))
		race.Disable()
	}
	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()
	}
	if race.Enabled {
		race.Enable()
	}
}

// Get selects an arbitrary item from the Pool, removes it from the
// Pool, and returns it to the caller.
// Get may choose to ignore the pool and treat it as empty.
// Callers should not assume any relation between values passed to Put and
// the values returned by Get.
//
// If Get would otherwise return nil and p.New is non-nil, Get returns
// the result of calling p.New.
func (p *Pool) Get() interface{} {
	if race.Enabled {
		race.Disable()
	}
	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 race.Enabled {
		race.Enable()
		if x != nil {
			race.Acquire(poolRaceAddr(x))
		}
	}
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

func (p *Pool) getSlow() (x interface{}) {
	// See the comment in pin regarding ordering of the loads.
	size := atomic.LoadUintptr(&p.localSize) // load-acquire
	local := p.local                         // load-consume
	// Try to steal one element from other procs.
	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
}

// pin pins the current goroutine to P, disables preemption and returns poolLocal pool for the P.
// Caller must call runtime_procUnpin() when done with the pool.
func (p *Pool) pin() *poolLocal {
	pid := runtime_procPin()
	// In pinSlow we store to localSize and then to local, here we load in opposite order.
	// Since we've disabled preemption, GC cannot happen in between.
	// Thus here we must observe local at least as large localSize.
	// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
	s := atomic.LoadUintptr(&p.localSize) // load-acquire
	l := p.local                          // load-consume
	if uintptr(pid) < s {
		return indexLocal(l, pid)
	}
	return p.pinSlow()
}

func (p *Pool) pinSlow() *poolLocal {
	// Retry under the mutex.
	// Can not lock the mutex while pinned.
	runtime_procUnpin()
	allPoolsMu.Lock()
	defer allPoolsMu.Unlock()
	pid := runtime_procPin()
	// poolCleanup won't be called while we are pinned.
	s := p.localSize
	l := p.local
	if uintptr(pid) < s {
		return indexLocal(l, pid)
	}
	if p.local == nil {
		allPools = append(allPools, p)
	}
	// If GOMAXPROCS changes between GCs, we re-allocate the array and lose the old one.
	size := runtime.GOMAXPROCS(0)
	local := make([]poolLocal, size)
	atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
	atomic.StoreUintptr(&p.localSize, uintptr(size))         // store-release
	return &local[pid]
}

何时销毁Pool里的值

  • sync包在初始化时,会向Go语言运行时系统注册一个函数(poolCleanup),这个函数的功能是清除所有已创建的临时对象池中的值。在垃圾回收每次开始执行之前,都会执行这个函数。
  • 此外,sync包中有一个包级私有的全局变量(allPools []*Pool),这个变量代表了当前程序中使用的所有临时对象池的汇总,称为汇总列表。
  • 通常,在Put或者Get方法第一次调用时,这个池子就会被加到汇总列表中(它们都调用了pin方法,pin方法调用了pinSlow方法,pinSlow中有判断Pool.local是nil,就将池子加入allsPool),所以池清理函数可以访问到所以正在被真正使用的临时对象池。
  • 池清理函数会遍历allPools,对于每一个临时对象池,都会将池中所有的私有临时对象和共享临时对象列表设置为nil,然后再把池子中的所有本地池列表销毁,最后,把allPools重置为空切片。
  • 如果临时对象池以外的代码再对无对它们的引用,那么在稍后的垃圾回收中,这些临时对象就会被当作垃圾销毁。
func poolCleanup() {
	// This function is called with the world stopped, at the beginning of a garbage collection.
	// It must not allocate and probably should not call any runtime functions.
	// Defensively zero out everything, 2 reasons:
	// 1. To prevent false retention of whole Pools.
	// 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.
	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{}
}

var (
	allPoolsMu Mutex
	allPools   []*Pool
)

func init() {
	runtime_registerPoolCleanup(poolCleanup)
}

func indexLocal(l unsafe.Pointer, i int) *poolLocal {
	lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
	return (*poolLocal)(lp)
}

// Implemented in runtime.
func runtime_registerPoolCleanup(cleanup func())
func runtime_procPin() int
func runtime_procUnpin()

临时对象池存储所用的数据结构

  • 临时对象池中,local字段是个数组,长度localSize为Go语言调度器中P的数量,P的数量可以使用runtime.GOMAXPROCS(100)设置,默认为操作系统cpu核数。
  • P的存在很重要的原因是为了分散并发程序的执行压力,而让临时对象池中的本地池列表的长度与P的数量相同的主要原因也是分散压力,包括存储和性能。
  • local数组的元素实际上是poolLocal结构体,poolLocal内嵌了poolLocalInternal,poolLocalInternal包含三个字段:private用来存储私有临时对象,共享临时对象列表shared,还有一个嵌入字段互斥锁。
  • 实际上每个本地池都对应着一个P。一个goroutine想要真正运行就必须先与某个P产生关联,也就是说,一个正在运行的goroutine必然关联着某个P。
  • 在调用Put或Get方法时,总会先从本地池列表中获取与当前goroutine对应的本地池,依据的就是与当前goroutine关联的P的id
  • 即Put或Get方法会获取到哪一个本地池,完全取决于当前goroutine关联的P。
    Golang:sync.Pool_第1张图片
type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() interface{}
}

// Local per-P Pool appendix.
type poolLocalInternal struct {
	private interface{}   // Can be used only by the respective P.
	shared  []interface{} // Can be used by any P.
	Mutex                 // Protects shared.
}

type poolLocal struct {
	poolLocalInternal

	// Prevents false sharing on widespread platforms with
	// 128 mod (cache line size) = 0 .
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}

// from runtime
func fastrand() uint32

var poolRaceHash [128]uint64

// poolRaceAddr returns an address to use as the synchronization point
// for race detector logic. We don't use the actual pointer stored in x
// directly, for fear of conflicting with other synchronization on that address.
// Instead, we hash the pointer to get an index into poolRaceHash.
// See discussion on golang.org/cl/31589.
func poolRaceAddr(x interface{}) unsafe.Pointer {
	ptr := uintptr((*[2]unsafe.Pointer)(unsafe.Pointer(&x))[1])
	h := uint32((uint64(uint32(ptr)) * 0x85ebca6b) >> 16)
	return unsafe.Pointer(&poolRaceHash[h%uint32(len(poolRaceHash))])
}

临时对象池怎样利用内部数据结构存取

  • 本地池的private字段只能被与之对应的P所关联的goroutine访问,而shared字段可以被任意goroutine访问。
  • Put方法先试图把新的临时对象,存储到本地池的private字段中,以便在后面Get时,可以快速拿到一个可用值。当private字段是nil,就将参数存到private里。如果private不是nil,那么如果参数也不是nil,就将参数追加到shared字段中,追加时要使用互斥锁保护。
  • Get方法先尝试从private字段获取临时对象,如果是nil再从shared字段中获取,同样获取时要加锁。如果本地池的shared为空,那就遍历临时对象池的所有本地池,逐个搜索它们的shared的。只要发现有不为空的,就把该列表最后一个元素取出返回。
  • 如果所有都为空,那么就调用临时对象池的New。
    Golang:sync.Pool_第2张图片

思考题

  • 怎样保证一个临时对象池中总有比较充足的临时对象?
  • 初始化时指定New字段对应的函数返回一个新建临时对象。
  • 临时对象使用完毕时调用Put方法,把该临时对象放回临时对象池中。

你可能感兴趣的:(Golang)