sync之ONCE
如何在代码中执行某个函数只运行一次,特别是在go这种高并发的情况下。
go给出了一个解法,sync.Once就是用来解决这种问题的,我们常用来初始化配置等。
把源码整理一下,发现源码极其简单。
|
|
就是使用一个标志位来标识是否调用完成,那么在此基础上,提出了两个问题:
1、为什么使用atomic.LoadUint32来判断,为什么不使用互斥锁,或者是CAS?
2、为什么要使用defer?
这里我们先就第一个问题来解答,为什么要用atomic.LoadUint32。
首先我们要了解atomic.LoadUint32是什么。它是一个原子操作,基于汇编执行的,颗粒度小,而互斥锁的颗粒度其实是比较大的。
而CAS其实是atomic.CompareAndSwapUint32,意思就是Compare And Swap,把判断和赋值包装成一个原子操作。
那么可不可以这样实现:
|
|
看上去似乎可行,也只会执行一次,当o.done==0时,会赋值为1,然后执行f()。
其他并发请求时,会发现o.done=1,就不会执行f()。
其实这样是不行的。
当o.done判断为0时,立即设置为1,然后再执行f(),这样语义就不正确了。
因为Once不仅仅要求只执行一次,还要保证其他在执行这个函数的时候看到o.done==1的时候,f()已经完成了。
这就涉及到逻辑的正确性。
例如通过sync.Once来读取配置,如果调用sync.Once通知用户已经读取完成了,而实际上f()还在执行,那么这个逻辑其实是错误的。
那么sync.Once是如何解决这个问题的?
1、快路径:原子读取o.done的值,保证竞态条件正确
2、慢路径:用互斥锁来执行f(),执行完成后修改o.done
第一次可能在执行互斥锁的时候比较慢,但只要成功执行后,就不会走到互斥锁了,只会走到原子操作。
既然内部是使用互斥锁来保证代码的临界区,那么就不能嵌套锁
例如如下使用:
|
|
第二个问题比较简单,如果在执行f()发生panic,使用defer会保证o.done的正确性。
总结
1、Once对外提供f()只执行一次的语义
2、Once.Do返回后,f()只会被执行一次,如果没有执行完,会阻塞直到执行完毕
3、内部用互斥锁来保证逻辑的原子性,先执行 f() ,然后设置 o.done 标识位
4、f()中不能有锁,内部有锁嵌套可能会导致死锁
