直接说吧, 这个东西为什么存在, 为了解决什么问题:
假设我们需要频繁申请内存用于存放你的结构体, 而这个结构体本身是短命的, 可能这个请求过去你就不用了. 申请了这么多内存, 对于 GC 来说就是一种压力了. 针对这个问题, 如果我们能产生一个池子, 用于存放这些短命内存, 理想情况中下次请求来了, 直接从池子中拿就好了, 那么 GC 的时候我们直接清理池子就完事了, 算是一种 GC 的优化套路.
你天天用 ( 于 debug ) 的 fmt 就使用了这个东西, fmt 总是需要很多[]byte 对象, 但是用一次就申请一次内存显然是不现实的, 于是就整了个它, 每次需要[]byte 就从 ppFree 池中拿一个出来
fmt.Println() 调用 ->
fmt.Fprintln() 调用 ->
fmt.newPrinter() 调用 ->
ppFree.Get() 其中 -> ppFree := sync.Pool{...}
sync.Pool 的组件
sync.Pool 是全局的, 这个 Pool 的工作会跟所有的 P 打交道(GMP 中的 P). 尽管你可以针对不同场景申请不同的 Pool, 比如我们可以为对象 A 的存取设置一个 Pool, 再为对象 B 的存取设置一个 Pool. 但是同一个 Pool 是不能被复制的, 我们在这里留下了几个问题:
为什么 Pool 是全局的, Pool 是怎么跟 P 打交道的
为什么 Pool 不许被复制, 不许复制这个特性是怎么被保证的
我们先开看看它的组件, 先是顶层:
- type Pool struct {
- noCopy noCopy
- local unsafe.Pointer
- localSize uintptr
- New func() interface{}
- }
New :
Pool 池子涉及 "存"/"取" 两个操作, 这个 New 函数是针对取的, 假设我们现在池子空了, 取的东西是什么呢? 取的就是 New 的返回值, 算是一个新的, 当然如果你不设置 New 函数, 空池取出来的就是 nil
写了一个小例子, 看看: go-playground https://play.golang.org/p/7JkoS7cAgF3
local/localSize :
存 / 取, 存到哪儿? 从哪儿取? 刚刚说了 Pool 的工作是全局的, 是结合 P 发挥的. 我们往下走一步, 关于 P 你还记得吗? P 持有一个 G 队列, 并且针对某个固定的 P, 同一时刻下只会有一个 G 在运行.
这么说吧, 每个 P 都会有一个 "盒子", 存东西就是往这里面存, 现在假设我们是在 P1 中: G1 先来了, 从盒子里取走了这个东西, 等 G1 用完以后将东西放回盒子里. 按照 P 调度的原则, P 会在 G1 退出以后切换到 G2. 因为 G1 用完以后放回去了, 因此等到 G2 想用的时候, 东西还在盒子里 , G2 拿着用, 用完放回去, 执行完成退出, 切换 G3, 凡此以往下去, 这个东西在盒子里存 / 取 / 存 / 取的进行下去, 被这个 P 中的每一个 G 拿来使用, 而我们只需要分配一个内存给它就够了
如果每个 P 都有一个盒子, 那这么多个 P 的盒子就能组成一个盒子数组, 数组长度就是 P 的数量. ok, 这里的盒子数组对应到 Pool 里就是 local 属性, 数组长度 numOf(P)就是 localSize 属性
回顾一下:
我们给每个 P 都赋了一个盒子用于存东西, P 中的 G 会从盒子里存走对象, 因此我们说: Pool 的工作是关乎 P 的
如果我们存在两个一模一样的盒子, 那到底往哪儿存呢? 因此我们也说: 同一个 Pool 必须是全局唯一的, 且不能复制的
noCopy :
一个用于防止复制的东西, 刚刚说同一个 Pool 必须是全局唯一不能复制的, 如果我非要复制它呢? go 语言本身也没什么禁止拷贝的设定, 简单来说, noCopy 结构体实现了 sync.Locker 接口, go vet(一个用于检查源码中静态错误的工具)中约定: 任何包含了 sync.Locker 实例, 在 go vet 检查中就不能通过
关于 sync.Locker 实例到底能不能复制, 我写了一个小例子, 你可以点进去复制到你自己电脑上, 然后通过 go vet 来验证一下, 首先复制这一关就过不了, 其次 fmt.Printf 本身也需要值拷贝, 即使是这个值拷贝也过不了: go-playground https://play.golang.org/p/LDHIY-j_oBp
如果我真的复制锁了, 会发生什么: 死锁, 第二把锁想上锁之前需要等第一把锁解开(但是你没有意识到这一点), 这里是我写的另一个例子: go-playground https://play.golang.org/p/aj-rM9ow03V
到了这里, 总结一下(防止你已经晕了):
Pool 能存能取, 存到此 P 下的盒子里.
如果盒子是空的, 用 New 函数定义空盒子取出来的是什么
Pool 是关乎 P 的, 因此是全局的, 有一个锁用于保证每个 Pool 的唯一性
讨论讨论存取的过程
到这里原理已经介绍的差不多了, 但是本着搞艺术应有的精神, 我们决定还是继续看看存取是怎么进行的. 在说之前, 我们需要介绍一下这个 "盒子" 是什么样的.
- type poolLocal struct {
- poolLocalInternal
- pad []byte
- }
- type poolLocalInternal struct {
- private interface{}
- shared []interface{}
- Mutex
- }
这里比较关键的是 private/shared:
- + -- []shared -- data_2 -- data_3 ...
- |
- M1 -- P1 --poolLocal-- + -- private -- data_1
- |
- + -- G1 -- G2 -- G3 ...
- + -- []shared -- data_5 -- data_6 ...
- |
- M2 -- P2 --poolLocal-- + -- private -- data_4
- |
- + -- G4 -- G5 -- G6 ...
- M3/M4 ...
之前我们说 P 会往自己的盒子里存 / 取对象, 原来我们刚刚说了半天的盒子不止包含有一个舱位啊:
私有舱位(对应 private 字段), 容量 = 1
公共舱位(对应[]shared 字段), 容量 = 好多个
还有一个锁(对应 Mutex 字段)
关于为什么每个盒子里既有私有舱位, 同时又有公共舱位, 同时还要锁, 我们后面会说
存 - put
源码就不放了, 反正也没人看 (你看么? 反正我不怎么看) , 我就用语言描述一下存的过程大概经历了那些步骤吧:
如果要存的东西是个 nil, 退出
尝试获取当前 G 对应 P 下的盒子(也就是 poolLocal)
如果盒子里的私有舱位是空的, 那么优先存到自己的私有舱位里, 存好了以后退出
如果私有舱位并不是空的, 但还是要存, 这种时候我们会存到公共舱位里去, 存的过程还会加锁, 存完了在解锁,
锁的存在必要在 "取" 的环节里能看到
取 - get
相似的, 也是私有舱位优先于公共舱位的方案:
拿到自己的盒子
优先从私有舱位取东西出来, 取到了就退出
没取到? 试试自己的公共舱位呢? 上锁, 检查, 解锁
自己公共舱位也没有吗? 试试别人的公共舱位呢?
这里来了, 说明自己的公共舱位也不是只有自己能用, 公共舱位之所以公共, 是因为大家都可能会来检查你, 不同的 P 都可能来你的公共舱位取东西
尽管同一个 P 下不同的 G 是串行的, 但是 P 跟 P 之间可是实实在在的并行, 也就是真的可能存在争抢的情况
同时我们也看到了公共舱位本质上也只是一个[]interface{}, 并没有什么特别防止争抢的设定, 因此我们给它加个锁
别人也没有, 只能 New 一个出来了
那么为什么会出现共享区呢 ?
如果按照我们之前分析的, 存 / 取 / 存 / 取的模式, 如果真的是这样, 你只管使用自己盒子里的东西就够用了, 为什么还要共享区呢? 我认为可能的原因是这样:
抢占调度
抢占调度的存在使得 G1 都没执行完, 东西都还没放回盒子里, G2 就上场了, 这个时候 G2 想从盒子里取东西来用, 发现没有(因为 G1 还没放回去), 那就 New 一个出来, 等 G2 执行完了把东西放回盒子里. 完事儿切换回 G1, G1 也用完了, 结果发现盒子里已经有 G2 刚刚放下去的东西了, 那怎么办呢, 只能放到共享区里了
如果需求量不确定呢?
我们只是设想, G1/G2 用一个东西, 那如果 G 需求多于一个呢? 要用好几个, 或者不确定个, 这时候可以从共享区拿
来源: http://www.tuicool.com/articles/eMnMBvi