NOTE

2.5 sync.pool

1. 是什么 保存temp object的pool。 temp表示这个对象可能会被回收,所以这个pool其实就是个cache 2. 为什么需要 将暂时不用的对象缓存起来,待下次需要的时候直接使用 这样就不用经过内存分配,减轻GC的压力 3. 怎么使用 3.1. fmt - Printf - Fpri

Go创建于 更新于 historical

这是历史学习笔记,可能存在过时或不完整的理解。

1. 是什么

保存temp object的pool。 temp表示这个对象可能会被回收,所以这个pool其实就是个cache

2. 为什么需要

将暂时不用的对象缓存起来,待下次需要的时候直接使用 这样就不用经过内存分配,减轻GC的压力

3. 怎么使用

package main

import (
	"fmt"
	"sync"
)

var pool *sync.Pool

type Person struct {
	Name string
}

func (p *Person)Reset() {
    p.Name = ""
}

//初始化pool。需要设置号New函数
func initPool() {
	pool = &sync.Pool{
		New: func() interface{} {
			fmt.Println("Creating a new Person")
			return new(Person)
		},
	}
}

func main() {
	initPool()
	//第一次调用Get方法,没有存货那么调用New函数创建对象
	p := pool.Get().(*Person)
	fmt.Println("首次从 pool 里获取:", p)
    //初始化
    p.Reset()
	p.Name = "first"
	fmt.Printf("设置 p.Name = %s\n", p.Name)
	//用完了之后放回池子
	pool.Put(p)
	//第二次调用Get方法,有存货直接返回
	fmt.Println("Pool 里已有一个对象:&{first},调用 Get: ", pool.Get().(*Person))
	//第三次调用Get方法,没有存货那么调用New函数创建对象
	fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}

3.1. fmt

  • Printf
func Printf(format string, a ...interface{}) (n int, err error) {
    //输出到os.Stdout--标准输出
	return Fprintf(os.Stdout, format, a...)
}
  • Fprintf
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()//创建pp
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()//将 pp 指针归还到 Pool 中
	return
}

func newPrinter() *pp {
    //其实是从Pool中获取pp
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}


func (p *pp) free() {
	if cap(p.buf) > 64<<10 {
		return
	}

    //归还到 Pool 前将对象的一些字段清零
	p.buf = p.buf[:0]
	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErr = nil
	ppFree.Put(p)
}

4. 源码分析

4.1. 数据结构

  • Pool
type Pool struct {
	noCopy noCopy

    // 每个 P 的本地队列,实际类型为 [P]poolLocal
	local     unsafe.Pointer // 指向 [P]poolLocal数组
	// [P]poolLocal的大小
	localSize uintptr        // local数组的大小

    //在一轮 GC 到来时,victim 和 victimSize 会分别“接管” local 和 localSize。victim 的机制用于减少 GC 后冷启动导致的性能抖动,让分配对象更平滑
	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array

	// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数
	New func() interface{}
}
  • poolLocal
// Local per-P Pool appendix.
type poolLocalInternal struct {
	private interface{} //  P 的私有缓存区,使用时无需要加锁
	shared  poolChain   // 公共缓存区。本地 P 可以 pushHead/popHead;其他 P 则只能 popTail
}

type poolLocal struct {
	poolLocalInternal

	// 将 poolLocal 补齐至两个缓存行的倍数,防止 false sharing,
	// 每个缓存行具有 64 bytes,目前我们的处理器一般拥有32KB缓存,所以有32 * 1024 / 64 = 512 条缓存行
	// 伪共享,仅占位用,防止在 cache line 上分配多个 poolLocalInternal
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
  • poolChain
type poolChain struct {
	// 只有生产者会 push to,不用加锁
	head *poolChainElt

	// 读写需要原子控制。 pop from
	tail *poolChainElt
}

type poolChainElt struct {
	poolDequeue

	// next 被 producer 写,consumer 读。所以只会从 nil 变成 non-nil
	// prev 被 consumer 写,producer 读。所以只会从 non-nil 变成 nil
	next, prev *poolChainElt
}


//poolDequeue 被实现为单生产者、多消费者的固定大小的无锁(atomic 实现) Ring 式队列(底层存储使用数组,使用两个指针标记 head、tail)。生产者可以从 head 插入、head 删除,而消费者仅可从 tail 删除。
type poolDequeue struct {

    //headTail 指向队列的头和尾,通过位运算将 head 和 tail 存入 headTail 变量中。
	// headTail 包含一个 32 位的 head 和一个 32 位的 tail 指针。这两个值都和 len(vals)-1 取模过。
	// tail 是队列中最老的数据,head 指向下一个将要填充的 slot
    // slots 的有效范围是 [tail, head),由 consumers 持有。
	headTail uint64

	// vals 是一个存储 interface{} 的环形队列,它的 size 必须是 2 的幂
	// 如果 slot 为空,则 vals[i].typ 为空;否则,非空。
	// 一个 slot 在这时宣告无效:tail 不指向它了,vals[i].typ 为 nil
	// 由 consumer 设置成 nil,由 producer 读
	vals []eface
}

4.2. 方法

4.2.1. Get

func (p *Pool) Get() interface{} {
    // ......
    //调用 p.pin() 函数将当前的 goroutine 和 P 绑定,禁止被抢占,返回当前 P 对应的 poolLocal,以及 pid
	l, pid := p.pin()
	//然后直接取 l.private,赋值给 x,并置 l.private 为 nil
	x := l.private
	l.private = nil
	if x == nil {
	    //判断 x 是否为空,若为空,则尝试从 l.shared 的头部 pop 一个对象出来,同时赋值给 x
		x, _ = l.shared.popHead()
		if x == nil {
		    //如果 x 仍然为空,则调用 getSlow 尝试从其他 P 的 shared 双端队列尾部“偷”一个对象出来
			x = p.getSlow(pid)
		}
	}
	//Pool 的相关操作做完了,调用 runtime_procUnpin() 解除非抢占
	runtime_procUnpin()
    // ......
    //最后如果还是没有取到缓存的对象,那就直接调用预先设置好的 New 函数,创建一个出来
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

4.2.2. Put

// src/sync/pool.go

// Put 将对象添加到 Pool 
func (p *Pool) Put(x interface{}) {
	if x == nil {
		return
	}
	// ……
	//先绑定 g 和 P,然后尝试将 x 赋值给 private 字段。
	l, _ := p.pin()
	if l.private == nil {
		l.private = x
		x = nil
	}
	//如果失败,就调用 pushHead 方法尝试将其放入 shared 字段所维护的双端队列中。
	if x != nil {
		l.shared.pushHead(x)
	}
	runtime_procUnpin()
    //…… 
}

4.3. GC

所有的池技术中,都会在某个时刻清除缓存对象。

pool.go的init函数

func init() {
    //注册了GC发生的时候如何清理Pool的函数
	runtime_registerPoolCleanup(poolCleanup)
}
  • poolCleanup
func poolCleanup() {
    //清空old pools
	for _, p := range oldPools {
		p.victim = nil
		p.victimSize = 0
	}

	// Move primary cache to victim cache.
	//把所有的pool丢到victim中
	for _, p := range allPools {
		p.victim = p.local
		p.victimSize = p.localSize
		p.local = nil
		p.localSize = 0
	}

	oldPools, allPools = allPools, nil
}

5. 总结

  • sync.Pool 是协程安全的,使用起来非常方便。设置好 New 函数后,调用 Get 获取,调用 Put 归还对象
  • 不要对 Get 得到的对象有任何假设,更好的做法是归还对象时,将对象“清空”
  • Pool 里对象的生命周期受 GC 影响,不适合于做连接池,因为连接池需要自己管理对象的生命周期

6. 参考