主要结构
// 第一次使用时,不能复制
type Pool struct {
noCopy noCopy //空结构,用来防止pool在第一次使用时被复制。
local unsafe.Pointer // 本地固定大小的pool池,其实类型为[P]poolLocal
localSize uintptr // 本地固定pool池的大小
victim unsafe.Pointer // 表示上一个周期的local
victimSize uintptr // 同上
// New 在 pool 中没有获取到,调用该方法生成一个变量
New func() interface{}
}
//具体存储结构
type poolLocalInternal struct {
private interface{} // 私有的,只能由自己的 P 使用
shared poolChain // 共享的,可以被任何的 P 使用
}
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
// 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
主体流程
看完整个结构后,我们先了解一下整个流程。
PUT方法
func (p *Pool) Put(x interface{}) {
...
//获取当前p的pool(池)
l, _ := p.pin()
if l.private == nil {
// 如果私有属性为nil,那么将x写入到private
l.private = x
x = nil
}
if x != nil {
//如果x没有复制给私有属性,则将其推到共享属性中
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
GET方法
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
// 获取当前p的pool(池)
l, pid := p.pin()
x := l.private
l.private = nil
if x == nil {
// 从当前 P 的 shared 末尾取一个
x, _ = l.shared.popHead()
if x == nil {
// 还没有取到 则去其他 P 的 shared 取
x = p.getSlow(pid)
}
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
if x != nil {
race.Acquire(poolRaceAddr(x))
}
}
// 最后还没取到 调用 NEW 方法生成一个
if x == nil && p.New != nil {
x = p.New()
}
return x
}
基准测试
package main
import (
"sync"
"testing"
)
type S struct {
num int
}
func BenchmarkWithPool(b *testing.B) {
var s *S
var pool = sync.Pool{
New: func() interface{} { return new(S) },
}
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
s = pool.Get().(*S)
s.num = 1
s.num++
pool.Put(s)
}
}
}
func BenchmarkWithNoPool(b *testing.B) {
var s *S
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
s = &S{num: 1}
s.num++
}
}
}
$ go test -bench=. -benchmem
goos: darwin
goarch: amd64
BenchmarkWithPool-4 10000 253269 ns/op 0 B/op 0 allocs/op
BenchmarkWithNoPool-4 10000 175742 ns/op 80000 B/op 10000 allocs/op
可以看到每次分配的内存 0 B vs 80000 B,每次内存分配次数 0 vs 10000。因为每次测试,我们执行了10000次迭代,所以看到没使用池的内存单次分配是 8B(即 结构 S 占的内存),单次分配次数为 1次。但是在每次执行的时间上使用池比不使用池是要多的,比较使用池涉及到池的维护,也算是正常的。这样看来,在高并发的场景下,context 的复用率非常高,所带来的 GC 压力也更小,所以效率当然就高了。