内容简介:在 echoEcho 的路由基于对于高并发的应用来说,大量的 goroutines 的内存申请确实是个负担。想知道为什么需要用 sync.pool,了解代码中为什么使用了这个模块会提高性能,以及有哪些注意点,需要对 sync.pool 有一定的熟悉。先来看看它是如何实现的。
在 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 压力也更小,所以效率当然就高了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用动态分析技术分析 Java
- 使用动态分析技术分析 Java
- 案例分析:如何进行需求分析?
- 深度分析ConcurrentHashMap原理分析
- 如何分析“数据分析师”的岗位?
- EOS源码分析(3)案例分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员的思维修炼
Andy Hunt / 崔康 / 人民邮电出版社 / 2010-12-10 / 39.00元
本书解释了为什么软件开发是一种精神活动,思考如何解决问题,并就开发人员如何能更好地开发软件进行了评论。书中不仅给出了一些理论上的答案,同时提供了大量实践技术和窍门。 本书供各层次软件开发人员阅读。一起来看看 《程序员的思维修炼》 这本书的介绍吧!