为什么Gin使用Sync.Pool呢
Sync.Pool
准备用两篇文章给大家讲解,第一篇也就是本篇主要讲解什么是Pool,为什么使用它以及那些框架或者标准库使用它;第二篇就深入源码为大家讲解Pool利用Ringbuffer
和双向链表
解决无锁并发
问题。
1. 什么是sync.Pool
sync.Pool 是 Golang 内置的对象池技术,可用于缓存临时对象,避免因频繁建立临时对象所带来的消耗以及对 GC 造成的压力。在许多知名的开源库中,都可以看到 sync.Pool 的大量使用。例如,HTTP 框架 Gin 用 sync.Pool 来复用每个请 求都会创建的 gin.Context 对象
。在 fmt、echo、fasthttp 等也都可以看到 sync.Pool 的身影
。
2. 为什么使用sync.Pool
golang提供了自动GC功能,好处是开发人员不需要考虑内存的分配释放,提升了构建程序的效率,但是在频繁分配内存的 场景中,GC的负载也会上升,表现为GC延迟高、程序性能下降。这个时候内建的 sync.Pool 是一个很好的选择。它 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压 力,提升系统的性能。
2.1 单元测试的表现
type Person struct { Age int } var personPool = sync.Pool{ New: func() interface{} { return new(Person) }, } //没有使用Sync.Pool的 func BenchmarkWithoutPool(b *testing.B) { var p *Person b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < 10000; j++ { p = new(Person) p.Age = 23 } } } //带有Sync.Pool的对象 func BenchmarkWithPool(b *testing.B) { var p *Person b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < 10000; j++ { p = personPool.Get().(*Person) p.Age = 23 personPool.Put(p) } } }
结果:
2.2 结论
我们可以看到当程序中频繁申请对象的时候,利用Sync.Pool对象池化技术比不使用Pool技术的程序性能高出两倍
,而且在占用内存方面池化技术因为复用内存对象而消耗的内存是0B
,没有使用Pool池化的程序消耗内存是1.6w B内存
。明眼人都能看出来利用池化技术带来的性能提升,不管是消耗时间上还是内存分配上
。
3. Sync.Pool在Gin中的使用
一个典型的web service流程如下:
- Gin server接受requset,从context pool中取出context赋值给gin.Context
- Gin将context传入http handler开始业务逻辑处理
- 经过一系列流程后,我们可能需要以异步的方式开启goroutine继续处理业务逻辑,比如分发消息到mq等,但是这里有问题哈,我们下面会说到。
- 在流程末尾,将对象归还pool以便重用。
步骤3这里的问题主要是ctx对象无法做到复用了,这里因为context在流程末尾已经被gin回收到pool中,此时context的状态是未知的,可能被回收或者被重置后放到其他 groutine中,在异步任务中读取会引起业务错误。
解决方案:
- 在需要异步的地方copy对象
- 使用其他的pool组件或者自己实现
Gin框架因为需要给每个请求分配Context,当百万并发到来时,频繁的创建对象会给golang的GC带来非常大的压力,因此Gin作者就利用Pool技术将Context对象复用起来,这样不但可以提升性能,而且在一定程度上可以缓解GC的压力。
4. 别的库也使用到了Sync.Pool
fmt
、echo
、fasthttp
等。
fmt包中的使用:
ppFree通过Get方法获取一个对象,在Sprintf中使用这个对象,使用完成之后调用free释放掉,即归还给Pool中。
5. 小结
Sync.Pool使用起来非常简单,大家可以结合自己的场景去使用,相信会给你们带来不一样的惊喜。下篇文章重点介绍Pool利用ring buffer和双向链表技术解决无锁并发问题
,欢迎大家关注,点赞和转发。