

golang sync.Pool 分析
source link: https://www.tuicool.com/articles/BJZr63y
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 echo 官网的手册 上可以看到 echo 框架的路由性能主要依赖于 radix tree 和 sync.pool 对内存的复用。
Echo 的路由基于 radix tree ,它让路由的查询非常快。路由使用了 sync pool 来重复利用内存并且几乎达到了零内存占用。
对于高并发的应用来说,大量的 goroutines 的内存申请确实是个负担。想知道为什么需要用 sync.pool,了解代码中为什么使用了这个模块会提高性能,以及有哪些注意点,需要对 sync.pool 有一定的熟悉。先来看看它是如何实现的。
基于 Go 1.12 版本
主要结构
type Pool struct { noCopy noCopy // noCopy 是一个空结构,用来防止 pool 在第一次使用后被复制 local unsafe.Pointer // per-P pool, 实际类型为 [P]poolLocal localSize uintptr // local 的 size // New 在 pool 中没有获取到,调用该方法生成一个变量 New func() interface{} } // 具体存储结构 type poolLocalInternal struct { private interface{} // 只能由自己的 P 使用 shared []interface{} // 可以被任何的 P 使用 Mutex // 保护 shared 线程安全 } type poolLocal struct { poolLocalInternal // 避免缓存 false sharing,使不同的线程操纵不同的缓存行,多核的情况下提升效率。 pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte } var ( allPoolsMu Mutex allPools []*Pool // 池列表 )
关于缓存 false sharing 的文章可以参考我的 译文
主体流程
看完整个结构后,我们先了解一下整个流程。
Put 方法
Put 方法的整个流程比较简单,主要是将用完的对象放回池中,看一下注释就可以理解。
func (p *Pool) Put(x interface{}) { ... // 获取当前 P 的 pool l := p.pin() // 私有属性为空 放入 if l.private == nil { l.private = x x = nil } runtime_procUnpin() // 私有属性放入失败 放入 shared 池 if x != nil { l.Lock() l.shared = append(l.shared, x) l.Unlock() } ... }
Get 方法
我们找到对应的代码如下,
func (p *Pool) Get() interface{} { ... // 获取当前 P 的 poolLocal l := p.pin() // 先从 private 读取 x := l.private l.private = nil runtime_procUnpin() // private 没有 if x == nil { l.Lock() // 从当前 P 的 shared 末尾取一个 last := len(l.shared) - 1 if last >= 0 { x = l.shared[last] l.shared = l.shared[:last] } l.Unlock() // 还没有取到 则去其他 P 的 shared 取 if x == nil { x = p.getSlow() } } ... // 最后还没取到 调用 NEW 方法生成一个 if x == nil && p.New != nil { x = p.New() } return x }
上面有一个 p.getSlow()
操作是说从其他的 P 中偷取一个,比较有意思,在 Go 的GMP模型中也存在这个偷的概念,基本和这个类似。我们来看看
func (p *Pool) getSlow() (x interface{}) { ... // 尝试从其他 P 中窃取一个元素。 pid := runtime_procPin() runtime_procUnpin() for i := 0; i < int(size); i++ { // 获取其他 P 的 poolLocal l := indexLocal(local, (pid+i+1)%int(size)) l.Lock() last := len(l.shared) - 1 if last >= 0 { x = l.shared[last] l.shared = l.shared[:last] l.Unlock() break } l.Unlock() } return x }
存活周期以及内存回收
在倒入 pool 包时执行的 init 函数会向 GC 注册 poolCleanup
函数,也就是在 GC 之前会运行该函数。
func init() { runtime_registerPoolCleanup(poolCleanup) }
我们来看看 poolCleanup,该函数主要是将所有池的变量解除引用,为下一步的 GC 作准备。
func poolCleanup() { // 在 GC 时会调用此函数。 // 它不能分配,也不应该调用任何运行时函数。 // 防御性地将所有东西归零,原因有两个: // 1. 防止整个池的错误保留。 // 2. 如果GC发生时goroutine与Put / Get中的l.shared一起使用,它将保留整个Pool。因此下一周期内存消耗将增加一倍。 for i, p := range allPools { // 将所有池对象接触引用 等待 GC 回收 allPools[i] = nil for i := 0; i < int(p.localSize); i++ { l := indexLocal(p.local, i) l.private = nil for j := range l.shared { l.shared[j] = nil } l.shared = nil } p.local = nil p.localSize = 0 } allPools = []*Pool{} }
整个流程图

image.png
echo 中的用途
在 echo 中主要用来存储 context,因为大量的 foroutines 不断申请 context 的内存,会给 GC 带来大的压力影响性能。所以 echo 采用 sync.pool 来优化。
// New creates an instance of Echo. func New() (e *Echo) { ... e.pool.New = func() interface{} { return e.NewContext(nil, nil) } e.router = NewRouter(e) return } // NewContext returns a Context instance. func (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) Context { return &context{ request: r, response: NewResponse(w, e), store: make(Map), echo: e, pvalues: make([]string, *e.maxParam), handler: NotFoundHandler, } } // AcquireContext returns an empty `Context` instance from the pool. // You must return the context by calling `ReleaseContext()`. func (e *Echo) AcquireContext() Context { return e.pool.Get().(Context) } // ReleaseContext returns the `Context` instance back to the pool. // You must call it after `AcquireContext()`. func (e *Echo) ReleaseContext(c Context) { e.pool.Put(c) }
看完定义,我们再看看,echo 里的使用。也就是说我们通过 pool 这种形式避免了在并发大的情况下,造成的内存申请,和 GC 的压力。
// http 请求处理方法 func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 从池里获取一个 context 对象 c := e.pool.Get().(*context) // 重置对象 c.Reset(r, w) ... // 用完后把 context e.pool.Put(c) }
基准测试
既然说 pool 的优势这么大,我们可以用基准测试来看一下 使用池和不实用池的区别。这里我们声明了一个非常简单的结构 S
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 压力也更小,所以效率当然就高了。
Recommend
-
69
上一次写了 sync.Mutex 的实现与演进后,感觉有必要对 GO 标准库的一些功能进行追踪,看看不断优化的过程,发掘有意思的点。一般 sync.Pool 用作小对像池,比如前公司同事,在...
-
69
看gin源码时发现了 sync.Pool 的使用 // gin.go:L144 func New() *Engine { ... engine.pool.New = func() interface{} { return engine.allocateContext() } return engine } // gin...
-
55
原文地址: medium.com/@blanchon.v… 原文作者:
-
43
前言 在 golang 中有一个池,它特别神奇,你只要和它有个约定,你要什么它就给什么,你用完了还可以还回去,但是下次拿的时候呢,确不一定是你上次存的那个,这个池就是 sync.Pool 说实话第一次看到这个东西的时候,...
-
23
最近在工作中碰到了 GC 的问题:项目中大量重复地创建许多对象,造成 GC 的工作量巨大,CPU 频繁掉底。准备使用 sync.Pool 来缓存对象,减轻 GC 的消耗。为了用起来更顺畅,我特地研究了一番,形成此文。本文从使用到源码解...
-
20
Golang sync.Pool源码阅读与分析 Go的很多地方都有用到 sync.Pool,这是作为一个内存池来使用的。例如 fmt.Printf: // These routines end in 'f' and take a format string. // Fprintf formats according to a form...
-
9
golang sync .pool Stevennnmmm · 大约15小时之前 · 29 次点击 · 预计阅读时间 9 分钟 · 不到1分钟之前 开始浏览
-
9
深度分析 Golang Sync.Pool 底层原理 发表于 2021-07-18 分类于
-
5
golang标准库sync.Pool原理及源码简析 pool关键作用: 减轻GC的压力。 复用对象内存。有时不一定希望复用内存,单纯是想减轻GC压力也可主动给pool塞对象。 Pool’s purpose is to...
-
6
0x00 前言 前文 Golang 中的 sync.Pool 使用 介绍了 sync.Pool 的使用及若干细节。 sync.Pool...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK