1

Go版本1.13.1

Go中有sync.Pool类型,我们可以把它理解成存放临时值的容器,之所以加上“临时”两个字,是因为它会在GC过程的STW步骤被清理。

sync.Pool类型使用前可以给它的New字段赋值,New字段类型是func() interface{},一个函数类型,该函数一般在池内为空的时候才会调用

sync.Pool有两个公开的方法,一个Put,一个Get,作用看函数名就知道了

Go的并发模型是GMP模型,sync.Pool给每个P都建立了本地池,一个本地私有池,一个本地共享池,执行Get方法时,先从本地私有池取,取不到,去本地共享池,再取不到,去其他P的共享池中取,失败的话去victim cache中取,再失败就调用New方法,New生成的对象不会放到本地池中,是直接返回给调用方的。

我今天在书上发现一个“坑”,其实是go版本的问题导致的,书中的例子大概是这样的

package main

import (
    "fmt"
    "runtime"
    "runtime/debug"
    "sync"
    "sync/atomic"
)

var (
    count int32
    initFunc = func() interface{} {
        return atomic.AddInt32(&count, 1)
    }

    pool = sync.Pool{New:initFunc}

)
func main() {
    debug.SetGCPercent(debug.SetGCPercent(-1))

    v1 := pool.Get()
    fmt.Printf("value 1: %v\n", v1)
    pool.Put(10)
    pool.Put(11)
    pool.Put(12)
    v2 := pool.Get()
    fmt.Printf("value 2: %v\n", v2)

    runtime.GC()

    v3 := pool.Get()
    fmt.Printf("value 3: %v\n", v3)
    pool.New = nil
    v4 := pool.Get()
    fmt.Printf("value 4: %v\n", v4)
}
// 书中的输出结果
value 1: 1
value 2: 10
value 3: 2
value 4: <nil>

// 我实际的输出结果
value 1: 1
value 2: 10
value 3: 11
value 4: 12

例子里是想展示GC时会清空Pool里面的元素,清空后会调用New方法
我实际执行发现结果和书上的不一样,我当时在想怎么会出现这种情况,难道GC有问题?后来经过翻看源码,查看GC日志,发现一个关键的代码段,sync.Pool在STW时会执行poolCleanup函数

func poolCleanup() {
    // Drop victim caches from all pools.
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }

    // Move primary cache to victim cache.
    for _, p := range allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }
    oldPools, allPools = allPools, nil
}

问题就出现在上面这个函数中,我这个版本的go,sync.Pool在GC时,数据会转到victim里面,也就是说会幸存一次GC,所以要实现书中的效果,需要两次GC。

后面去查看go代码提交日志,确实发现了这个代码的提交记录

总结

总结一下sync.Pool的两个特性

  1. 对垃圾回收友好
  2. 可以把对象值产生的存储压力进行分摊

iuoui
120 声望10 粉丝