深入理解 sync.Once 与 sync.Pool
深入理解 sync.once 与 sync.pool
sync.once
代表在这个对象下在这个示例下多次执行能保证只会执行一次操作。
var once sync.once for i:=0; i < 10; i++ { once.do(func(){ fmt.println("execed...") }) }
在上面的例子中,once.do 的参数 func 函数就会保证只执行一次。
sync.once 原理
那么 sync.once 是如何保证 do 执行体函数只执行一次呢?
从 sync.once 的源码就可以看出其实就是通过一个 uint32 类型的 done 标识实现的。当 done = 1
就标识着已经执行过了。once 的源码非常简短
package sync import ( "sync/atomic" ) type once struct { done uint32 m mutex } func (o *once) do(f func()) { if atomic.loaduint32(&o.done) == 0 { o.doslow(f) } } func (o *once) doslow(f func()) { o.m.lock() defer o.m.unlock() if o.done == 0 { defer atomic.storeuint32(&o.done, 1) f() } }
do
方法内部用到了内存加载同步原语 atomic.loaduint32
,done = 0
表示还没有执行,所以多个请求在 f
执行前都会进来执行 o.doslow(f)
,然后通过互斥锁使保证多个请求只有一个才能成功执行,保证了 f 成功返回之后才会内存同步原语将 done
设置为 1。最后释放锁,后面的请求就因无法满足判断而退出。
如果仔细查看源代码中的注释就会发现 go 团队还解释了为什么没有使用 cas 这种同步原语实现。因为 sync.once
的 do(f)
在执行的时候要保证只有在 f 执行完之后 do 才返回。想象一下有至少两个请求,do 是用 cas 实现的:
func (o *once) do(f func()) { if atomic.compareandswapuint32(&o.done, 0, 1) { f() } }
虽然 cas 保证了同一时刻只有一个请求进入 if 判断执行 f()。但是其它的请求却没有等待 f() 执行完成就立即返回了。那么用户端在执行 once.do 返回之后其实就可能存在 f() 还未完成,就会出现意料之外的错误。如下面例子
var db sqldb var once sync.once for i:=0; i < 2; i++ { once.do(func() { db = newsqldb() fmt.println("execed...") }) } // #1 db.query("select * from table") ...
根据上述如果是用 cas 实现的 once,那么当 once.do
执行完返回并且循环体结束到达 #1 时,由于 db 的初始化函数可能还没完成,那么这个时候 db 还是 nil,那么直接调用 db.query
就会发生错误了。
sync.once 使用限制
由于 go 语言一切皆 struct 的特性,我们在使用 sync.once 的时候一定要注意不要通过传递参数使用。因为 go 对于 sync.once 参数传递是值传递,会将原来的 once 拷贝过来,所以有可能会导致 once 会重复执行或者是已经执行过了就不会执行的问题。
func main() { for i := 0; i < 10; i++ { once.do(func() { fmt.println("execed...") }) } duplicate(once) } func duplicate(once sync.once) { for i := 0; i < 10; i++ { once.do(func() { fmt.println("execed2...") }) } }
比如上述例子,由于 once 已经执行过一次,once.done 已经为 1。这个时候再通过传递,由于 once.done 已经为1,所以就不会执行了。上面的输出结果只会打印第一段循环的结果 execed...
。
sync.pool
sync.pool 其实把初始化的对象放到内部的一个池对象中,等下次访问就直接返回池中的对象,如果没有的话就会生成这个对象放入池中。pool 的目的是”预热“,即初始化但还未立即使用的对象,由于预先初始化至 pool,所以到后续取得时候就直接返回已经初始化过得对象即可。这样提高了程序吞吐,因为有时候在运行时初始化一些对象的开销是非常昂贵的,如数据库连接对象等。
现在我们来深入分析 pool
sync.pool 原理
sync.pool 核心对象有三个
- new:函数,负责对象初始化
- get:获取 pool 中的对象,如果 pool 中对象不存在则会调用 new
- put:将对象放入 pool 中
new func
pool 的结构很简单,就 5 个字段
type pool struct { ... new func() interface{} }
字段 new
是一个初始化对象的指针,该方法不是必填的,当没有设置 new 函数时,调用 get 方法会返回 nil。只有在指定了 new 函数体后,调用 get 如果发现 pool 中没有就会调用 new 初始化方法并返回该对象。
poollocalinternal
在将 get、put 之前得先了解 poollocalinternal 这个对象,里面只有两个对象,都是用来存储要用的对象的:
type poollocalinternal struct { private interface{} // can be used only by the respective p. shared poolchain // local p can pushhead/pophead; any p can poptail. }
操作这个对象时必须要把当前的 goroutine 绑定到 p,并且禁止让出 g。在 get 和 put 操作时都是优先操作 private
这个字段,只有在这个字段为 nil 的情况下才会转而读取 poolchain 共享链表,每读取操作都是一次 pop。
get
每个当前 goroutine 都拥有一个 poollocalinternal.private
,在 g 调用 get 方法时会做如下方法:
- 查询
private
是否有值,有直接返回;没有查询共享 poolchain 链表 - 如果 poolchain 链表 pop 返回的值不为 nil,则直接返回;如果没有值则转向其它 p 中的 poolchain 队列中存在的值
- 如果其它的 p 的共享队列中都没有值,就会尝试在主存中地址获取对应的值返回
- 最终都没有就会执行 new 函数体返回,没有设置 new 则返回 nil。
从上面的调用过程来看,pool.get 获取值的过程在一定程度与 gmp 模型有很多相似的地方的。
put
put 操作就比较简单了,优先将值赋值给 poollocalinternal.private
(同样是固定将当前的 g 绑定到 p 上),如果同时有多个值 put,那么就会将剩余的值插入到共享链表 poolchain
sync.pool 使用限制
因为 pool 每次的 get 操作都会将值 remove + return
,相当于用完即抛。并且要注意 get 的执行过程。put 方法的参数类型可以是任意类型,一定要切记不要将不同类型的值存进去。如果存在多协程(或循环)调用 get 时,你无法确定哪次调用的就是你想要的类型而导致出现未知的错误。
上一篇: Go常见并发模式