syncPoolsync.Pool
Pool
sync.Poolsync.Pool
使用姿势
初始化 Pool 实例 New
第一个步骤就是创建一个 Pool 实例,关键一点是配置 New 方法,声明 Pool 元素创建的方法。
申请对象 Get
Get
释放对象 Put
使用对象之后,调用 Put 方法声明把对象放回池子。注意了,这个调用之后仅仅是把这个对象放回池子,池子里面的对象啥时候真正释放外界是不清楚的,是不受外部控制的。
你看,Pool 的用户使用界面就这三个接口,非常简单,而且是通用型的 Pool 池模式,针对所有的对象类型都可以用。
思考
为什么用 Pool,而不是在运行的时候直接实例化对象呢?
本质原因:Go 的内存释放是由 runtime 来自动处理的,有 GC 过程。
举个栗子:
上面的例子可以直接复制运行起来看下,控制台输出:
go run
New
再来,如果我不用 Pool 来申请实例,而是直接申请,也就是上面的代码只改一行:
将以下代码:
修改成:
go run test_pool.go
注意到,和之前有两个不同点:
- 同样也是运行两次,两次结果相同。
- 对象创建的数量和并发 Worker 数量相同,数量等于 1048576 (这个就是 1024*1024);
createBuffer
实际上还有一个不同点,就是程序跑的过程中,该进程分配消耗的内存很大。因为 Go 申请内存是程序员触发的,回收却是 Go 内部 runtime GC 回收器来执行的,这是一个异步的操作。这种业务不负责任的内存使用会对 GC 带来非常大的负担,进而影响整体程序的性能。
类比现实的例子
一个程序猿喝奶茶,需要一个吸管(吸管类比就是我们代码里的 buffer 对象喽),奶茶喝完吸管就扔了,那就是塑料垃圾了( Garbage )。清洁工老李( GC 回收器 )需要紧跟在后面打扫卫生,现在 1048576 个程序猿同时喝奶茶,每个人都现场要一根新吸管,喝完就扔,马上地上有 1048576 个塑料吸管垃圾。清洁工老李估计要累个半死。
那如果某个隐秘的角落放一个回收箱 ( 类比成,sync.Pool ) ,程序员喝完奶茶之后,吸管就丢到回收箱里,下一个程序员要用吸管的话,伸手进箱子摸一下,看下有管子吗?有的话,就拿来用了。没有的话,就再找人要一根新吸管。这样新吸管的使用数量就大大减少了呀,多好呀。
并且,极限情况下,如果大家喝奶茶足够快,保证箱子里每时每刻都至少有一根用过的吸管,那 1048576 个程序员估计用一根吸管都够了。。。。(有点想吐)
回归正题
这就也解释了,为什么使用 sync.Pool 之后数量只有 3,4 个。但是进一步思考:为什么 sync.Pool 的两次使用结果输出不不一样呢?
因为复用的速度不一样。我们不能对 Pool 池里的 cache 的元素个数做任何假设。不过还是那句话,如果速度足够快,其实里面可以只有一个元素就可以服务 1048576 个并发的 Goroutine 。
sync.Pool
sync.Pool 当然是线程安全的。官方文档里明确说了:
A Pool is safe for use by multiple goroutines simultaneously.
但是,为什么我这里会单独提出来呢?
sync.PoolPoolPool.NewPool.New
createBuffernumCalcsCreatedatomic.AddInt32(&numCalcsCreated, 1)
numCalcsCreatedPool.NewcreateBuffer
atomic.AddInt32(&numCalcsCreated, 1)numCalcsCreated++go run -race test_pool.go
Pool.New
sync.Pool
因为,我们不能对 sync.Pool 中保存的元素做任何假设,以下事情是都可以发生的:
- Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
- Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 住的。使用者无法区分;
- Pool 池里面的元素个数你无法知道;
所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担。划重点:临时对象。所以说,像 socket 这种带状态的,长期有效的资源是不适合 Pool 的。
总结
sync.New
今天先从使用姿势,整体梳理学习了 sync.Pool 这个 package ,后面从实现原理深度剖析,敬请期待。
更多干货,关注公众号:奇伢云存储