sync包
data race
需要注意代码中得data race行为,这时候需要锁来控制,同时产生了读写,通常我们可以使用go test -race
来查询竞争行为。
detecting race conditions with go
sync.Pool
1. 概要
Go 并发相关库 sync 里面有一个有趣的 package Pool,sync.Pool
是个有趣的库,用很少的代码实现了很巧的功能。第一眼看到 Pool
这个名字,就让人想到池子,元素池化是常用的性能优化的手段(性能优化的几把斧头:并发,预处理,缓存)。比如,创建一个 100 个元素的池,然后就可以在池子里面直接获取到元素,免去了申请和初始化的流程,大大提高了性能。释放元素也是直接丢回池子而免去了真正释放元素带来的开销。
但是再仔细一看 sync.Pool
的实现,发现比我预期的还更有趣。sync.Pool
除了最常见的池化提升性能的思路,最重要的是减少 GC 。常用于一些对象实例创建昂贵的场景。注意,Pool 是 Goroutine 并发安全的。
2. 使用姿势
2.1 初始化 Pool 实例 New
第一个步骤就是创建一个 Pool 实例,关键一点是配置 New 方法,声明 Pool 元素创建的方法。
1 |
|
2.2 申请对象 Get
buffer := bufferPool.Get()
Get
方法会返回 Pool 已经存在的对象,如果没有,那么就走慢路径,也就是调用初始化的时候定义的 New 方法(也就是最开始定义的初始化行为)来初始化一个对象。
2.3 释放对象 Put
bufferPool.Put(buffer)
使用对象之后,调用 Put 方法声明把对象放回池子。注意了,这个调用之后仅仅是把这个对象放回池子,池子里面的对象啥时候真正释放外界是不清楚的,是不受外部控制的。
你看,Pool 的用户使用界面就这三个接口,非常简单,而且是通用型的 Pool 池模式,针对所有的对象类型都可以用。
3. 思考
3.1 为什么用 Pool,而不是在运行的时候直接实例化对象呢?
本质原因:Go 的内存释放是由 runtime 来自动处理的,有 GC 过程。
举个栗子:
1 |
|
上面的例子可以直接复制运行起来看下,控制台输出:
1 |
|
程序 go run
运行了两次,一次结果是 3 ,一次是 4 。这个是什么原因呢?
首先,这个是正常的情况,不知道你有没有注意到,创建 Pool 实例的时候,只要求填充了 New
函数,而根本没有声明或者限制这个 Pool 的大小。所以,记住一点,程序员作为使用方不能对 Pool 里面的元素个数做假定。
再来,如果我不用 Pool 来申请实例,而是直接申请,也就是上面的代码只改一行:
将以下代码:
1 |
|
修改成:
1 |
|
这个时候,我们再执行程序 go run test_pool.go
,会发现什么?
1 |
|
注意到,和之前有两个不同点:
- 同样也是运行两次,两次结果相同。
- 对象创建的数量和并发 Worker 数量相同,数量等于 1048576 (这个就是 1024*1024);
原因很简单,因为每次都是直接调用 createBuffer
函数申请 buffer,有 1048576 个并发 Worker 调用,所以跑多少次结果都会是 1048576。
实际上还有一个不同点,就是程序跑的过程中,该进程分配消耗的内存很大。因为 Go 申请内存是程序员触发的,回收却是 Go 内部 runtime GC 回收器来执行的,这是一个异步的操作。这种业务不负责任的内存使用会对 GC 带来非常大的负担,进而影响整体程序的性能。
3.2 sync.Pool 是并发安全的吗?
sync.Pool 当然是并发安全的。官方文档里明确说了:
A Pool is safe for use by multiple goroutines simultaneously.
但是,为什么我这里会单独提出来呢?
因为 sync.Pool
只是本身的 Pool
数据结构是并发安全的,并不是说 Pool.New
函数一定是线程安全的。**Pool.New**
函数可能会被并发调用 ,如果 New 函数里面的实现是非并发安全的,那就会有问题。
细心的小伙伴会注意到我在上面的代码例子里,关于 createBuffer
函数的实现里,对于 numCalcsCreated
的计数加是用原子操作的:atomic.AddInt32(&numCalcsCreated, 1)
。
1 |
|
因为 numCalcsCreated
是个全局变量,Pool.New
( 也就是 createBuffer
) 并发调用的时候,会导致 data race ,所以只有用原子操作才能保证数据的正确性。
小伙伴们可以尝试下,把 atomic.AddInt32(&numCalcsCreated, 1)
这样代码改成 numCalcsCreated++
,然后用 go run -race test_pool.go
命令检查一下,肯定会报告告警的,类似如下:
1 |
|
本质原因:Pool.New
函数可能会被并发调用。
3.3 为什么 sync.Pool 不适合用于像 socket 长连接或数据库连接池?
因为,我们不能对 sync.Pool 中保存的元素做任何假设,以下事情是都可以发生的:
- Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
- Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 住的。使用者无法区分;
- Pool 池里面的元素个数你无法知道;
所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担。划重点:临时对象。所以说,像 socket 这种带状态的,长期有效的资源是不适合 Pool 的。
4. 总结
- sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担;
- 不能对 Pool.Get 出来的对象做预判,有可能是新的(新分配的),有可能是旧的(之前人用过,然后 Put 进去的);
- 不能对 Pool 池里的元素个数做假定,你不能够;
- sync.Pool 本身的 Get, Put 调用是并发安全的,
sync.New
指向的初始化函数会并发调用,里面安不安全只有自己知道; - 当用完一个从 Pool 取出的实例时候,一定要记得调用 Put,否则 Pool 无法复用这个实例,通常这个用 defer 完成;