go sync.Pool介绍

是什么

Golang 来开发并构建高并发场景下的服务,但是由于 Golang 内建的GC机制多少会影响服务的性能,因此,为了减少频繁GC,Golang提供了对象重用的机制,也就是使用sync.Pool构建对象池。我们可以把sync.Pool类型值看作存放临时值的容器。此类容器是自动伸缩的、高效的、同时也是并发安全的。我们可以把sync.Pool称为“临时对象池”。

  • 以下摘自sync.Pool源码中的注释部分:

A Pool is a set of temporary objects that may be individually saved and retrieved.

Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.

A Pool is safe for use by multiple goroutines simultaneously.

Pool's purpose is to cache allocated but unused items for later reuse,
relieving pressure on the garbage collector. That is, it makes it easy to
build efficient, thread-safe free lists. However, it is not suitable for all
free lists.

An appropriate use of a Pool is to manage a group of temporary items
silently shared among and potentially reused by concurrent independent
clients of a package. Pool provides a way to amortize allocation overhead across many clients.

An example of good use of a Pool is in the fmt package, which maintains a dynamically-sized store of temporary output buffers. The store scales under load (when many goroutines are actively printing) and shrinks when quiescent.

On the other hand, a free list maintained as part of a short-lived object is
not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.

A Pool must not be copied after first use.

sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。这样就可以不用再次经过内存分配,可直接复用已有对象,减轻 GC 的压力,从而提升系统的性能。

怎么使用

package main

import (
    "fmt"
    "sync"
)

// 定义一个 Person 结构体,有Name和Age变量
type Person struct {
    Name string
    Age  int
}

// 初始化sync.Pool,new函数就是创建Person结构体
func initPool() *sync.Pool {
    return &sync.Pool{
        New: func() interface{} {
            fmt.Println("创建一个 person.")
            return &Person{}
        },
    }
}

// 主函数,入口函数
func main() {
    pool := initPool()
    person := pool.Get().(*Person)
    fmt.Printf("首次从sync.Pool中获取person::%#v\n", person)  // &main.Person{Name:"", Age:0}
    person.Name = "Jack"
    person.Age = 23
    pool.Put(person)
    fmt.Printf("Pool 中有一个对象,调用Get方法获取:%#v\n", pool.Get().(*Person))  //&main.Person{Name:"Jack", Age:23}
    fmt.Printf("Pool 中没有对象了,再次调用Get方法:%#v\n", pool.Get().(*Person)) // &main.Person{Name:"", Age:0}
}
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

    victim     unsafe.Pointer // local from previous cycle
    victimSize uintptr        // size of victims 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{}
}

sync.Pool{
    New: func() interface{} {
    
    },
}

func (p *Pool) Get() interface{} {}


func (p *Pool) Put(x interface{}) {}

Pool的struct的结构,New是唯一一个对外可访问的字段。赋给该字段的函数会被临时对象池用来创建对象值。该函数一般仅在池中无可用对象值的时候才被调用。

sync.Pool 有2个公开的指针方法。Get和Put。Get用于从pool中获取一个interface{}类型的值,而Put的作用则是把一个interface{}类型的值放置于池中。

源码简析

Get方法:

func (p *Pool) Get() interface{} {
  if race.Enabled {
    race.Disable()
  }
  l, pid := p.pin()
  x := l.private
  l.private = nil
  if x == nil {
    // Try to pop the head of the local shard. We prefer
    // the head over the tail for temporal locality of
    // reuse.
    x, _ = l.shared.popHead()
    if x == nil {
      x = p.getSlow(pid)
    }
  }
  runtime_procUnpin()
  if race.Enabled {
    race.Enable()
    if x != nil {
      race.Acquire(poolRaceAddr(x))
    }
  }
  if x == nil && p.New != nil {
    x = p.New()
  }
  return x
}

阅读以上Get方法的源码,可以知道:

  • 首先尝试从本地P对应的那个对象池中获取一个对象值, 并从对象池中删掉该值。
  • 如果从本地对象池中获取失败,则从共享列表中获取,并从共享列表中删除该值。
  • 如果从共享列表中获取失败,则会从其它P的对象池中“偷”一个过来,并删除共享池中的该值(就是源码中14行的p.getSlow())。
  • 如果还是失败,那么直接通过 New() 分配一个返回值,注意这个分配的值不会被放入对象池中。New()是返回用户注册的New函数的值,如果用户未注册New,那么默认返回nil。

put方法:

// 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
  }
  if x != nil {
    l.shared.pushHead(x)
  }
  runtime_procUnpin()
  if race.Enabled {
    race.Enable()
  }
}

阅读以上Put方法的源码可以知道:

  • 如果Put放入的值为空,则直接 return 了,不会执行下面的逻辑了;
  • 如果不为空,则继续检查当前goroutine的private是否设置对象池私有值,如果没有则将x赋值给该私有成员,并将x设置为nil;
  • 如果当前goroutine的private私有值已经被赋值过了,那么将该值追加到共享列表。

验证Pool 多线程

  • 未使用sync.Pool
package main
import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)

// 用来统计实例真正创建的次数
var numCalcsCreated int32

// 创建实例的函数
func createBuffer() interface{} {
    // 这里要注意下,非常重要的一点。这里必须使用原子加,不然有并发问题;
    atomic.AddInt32(&numCalcsCreated, 1)
    buffer := make([]byte, 1024)
    return &buffer
}

func main() {
    now := time.Now()
    // 多 goroutine 并发测试
    numWorkers := 1024 * 1024
    var wg sync.WaitGroup
    wg.Add(numWorkers)

    for i := 0; i < numWorkers; i++ {
        go func() {
            defer wg.Done()
            createBuffer()
        }()
    }
    wg.Wait()
    fmt.Println("legacy:", time.Since(now).Milliseconds())
    fmt.Printf("%d buffer objects were created.\n", numCalcsCreated)
}

输出结果:
legacy: 367
1048576 buffer objects were created.

  • 使用sync.Pool
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)

// 用来统计实例真正创建的次数
var numCalcsCreated int32

// 创建实例的函数
func createBuffer() interface{} {
    // 这里要注意下,非常重要的一点。这里必须使用原子加,不然有并发问题;
    atomic.AddInt32(&numCalcsCreated, 1)
    buffer := make([]byte, 1024)
    return &buffer
}

func main() {
    // 创建实例
    bufferPool := &sync.Pool{
        New: createBuffer,
    }

    now := time.Now()
    // 多 goroutine 并发测试
    numWorkers := 1024 * 1024
    var wg sync.WaitGroup
    wg.Add(numWorkers)

    for i := 0; i < numWorkers; i++ {
        go func() {
            defer wg.Done()

            //申请一个 buffer 实例
            buffer := bufferPool.Get()
            _ = buffer.(*[]byte)
            // 释放一个 buffer 实例
            defer bufferPool.Put(buffer)

            //createBuffer()
        }()
    }
    wg.Wait()
    fmt.Println("legacy:", time.Since(now).Milliseconds())
    fmt.Printf("%d buffer objects were created.\n", numCalcsCreated)
}

输出结果:
legacy: 172
8 buffer objects were created.

通过以上输出结果的对比,可以看到在多线程的情况下,使用sync.Pool可以明显减少创建对象的次数,从而减少程序运行时间。

验证Pool 单线程

package main

import (
    "encoding/json"
    "fmt"
    "sync"
    "time"
)

type Student struct {
    Name    string
    Class   int
    Teacher string
    Gender  int
    Age     int32
    Remark  [1024]byte
}

var buf, _ = json.Marshal(Student{Name: "Geektutu", Class: 5, Teacher: "testTeacher", Gender: 1, Age: 25})

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

func main() {
    num := 100000 // 10万次

    originUnmarshal(num)
    unmarshalWithPool(num)
}

// 反解json,未使用pool
func originUnmarshal(num int) {
    start := time.Now()
    for n := 0; n < num; n++ {
        stu := &Student{}
        json.Unmarshal(buf, stu)
    }
    fmt.Println("originUnmarshal duration:", time.Since(start).Milliseconds())
}

// 反解json,使用pool
func unmarshalWithPool(num int) {
    start := time.Now()
    for n := 0; n < num; n++ {
        stu := studentPool.Get().(*Student)
        json.Unmarshal(buf, stu)
        studentPool.Put(stu)
    }
    fmt.Println("unmarshalWithPool duration:", time.Since(start).Milliseconds())
}

输出结果:
originUnmarshal duration: 9719
unmarshalWithPool duration: 9466

通过以上结果可以看出,在单线程for循环中使用sync.pool的效果并不明显。

应用场景

sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担;
sync.Pool 中保存的元素有如下特征:

  • Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
  • Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 的,使用者无法区分
  • Pool 池里面的元素个数你无法知道;
    所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。

go源码中的实际使用

sync.Pool在go源码中的典型使用就是在fmt包中。

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

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

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

// free saves used pp structs in ppFree; avoids an allocation per invocation.
func (p *pp) free() {
    // Proper usage of a sync.Pool requires each entry to have approximately
    // the same memory cost. To obtain this property when the stored type
    // contains a variably-sized buffer, we add a hard limit on the maximum buffer
    // to place back in the pool.
    //
    // See https://golang.org/issue/23199
    if cap(p.buf) > 64<<10 {
        return
    }

    p.buf = p.buf[:0]
    p.arg = nil
    p.value = reflect.Value{}
    p.wrappedErr = nil
    ppFree.Put(p)
}

在源码中我们可以看到,fmt.Println方法。ppFree定义了一个sync.Pool的初始化方法,newPrinter从sync.Pool(ppFree)中获取,free() 用完后再放入sync.Pool(ppFree)中

参考

1、sync.Pool 复用对象
2、Golang之sync.Pool使用详解
3、Go语言 sync.Pool 应用详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342