是什么
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 应用详解