sync.Once相信很多人都用的较多,越看起来简单迷惑越大,别小看这数十行代码,蕴藏的东西可不少。鉴于以往的经验,这里进行分析汇总,致力于一文搞定这里的所有问题并避免误入歧途。是不是干货,你说了算。
目录
结构定义
- // Once is an object that will perform exactly one action.
- //
- // A Once must not be copied after first use.
- type Once struct {
- // done indicates whether the action has been performed.
- // It is first in the struct because it is used in the hot path.
- // The hot path is inlined at every call site.
- // Placing done first allows more compact instructions on some architectures (amd64/386),
- // and fewer instructions (to calculate offset) on other architectures.
- done uint32
- m Mutex
- }
核心方法
- // Do calls the function f if and only if Do is being called for the
- // first time for this instance of Once. In other words, given
- // var once Once
- // if once.Do(f) is called multiple times, only the first call will invoke f,
- // even if f has a different value in each invocation. A new instance of
- // Once is required for each function to execute.
- //
- // Do is intended for initialization that must be run exactly once. Since f
- // is niladic, it may be necessary to use a function literal to capture the
- // arguments to a function to be invoked by Do:
- // config.once.Do(func() { config.init(filename) })
- //
- // Because no call to Do returns until the one call to f returns, if f causes
- // Do to be called, it will deadlock.
- //
- // If f panics, Do considers it to have returned; future calls of Do return
- // without calling f.
- //
- func (o *Once) Do(f func()) {
- // Note: Here is an incorrect implementation of Do:
- //
- // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
- // f()
- // }
- //
- // Do guarantees that when it returns, f has finished.
- // This implementation would not implement that guarantee:
- // given two simultaneous calls, the winner of the cas would
- // call f, and the second would return immediately, without
- // waiting for the first's call to f to complete.
- // This is why the slow path falls back to a mutex, and why
- // the atomic.StoreUint32 must be delayed until after f returns.
-
- if atomic.LoadUint32(&o.done) == 0 {
- // Outlined slow-path to allow inlining of the fast-path.
- 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()
- }
- }
1,Once是一个将执行一个动作的对象。
2,如果 once.Do(f) 被多次调用,只有第一次调用会调用 f, 即使 f 在每次调用中具有不同的值,也就是说,先入为主,你其他人全部忽略。
3,如果f的执行发生了panic,Do认为它已经返回(已被调用过); 再次执行Do时不会再调用f,也就是说,好话不说2遍。
用原子操作判断了done属性,如果值为0才会去调用f,执行f后修改为1,否则什么也不做,因此这就决定了,Do中不论你放的是什么,要做几件事情,每件事情是否相同,它对这些都不关心对于单个once对象来说,只要你执行了Do,它就认为已执行过f,此后再次执行Do就不会再执行f了。
注意看这段:
- // Note: Here is an incorrect implementation of Do:
- //
- // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
- // f()
- // }
特地标注这种方式,实际就是为了避免错误的实现,为什么会错误?
来看这样一个场景(如果按先CompareAndSwapUint32的思路,CompareAndSwapUint32确实可以保证只执行一次,但,往下看):
do在执行时的时候要先判断是否需要继续往下走,而不是通过compare来直接比较交换,一开始如果直接CompareAndSwapUint32的话,第一次执行的执行者势必会把done变为1,在修改为1后、在执行f结束之前,如果有其他人继续执行once. do,会发现done值已经是1,因此其他人不会再执行f 就去直接使用了 f产生的实际结果,就会导致一些不堪设想的后果。
这里其他人指的就是不同的goroutine。
那如何保证第一个人执行f 期间,也就是执行完毕之前,其他人等第一个人执行结束再判断呢而不是发现是1就返回?
方式就是用互斥锁控制。
第二个人进来后发现临界区被锁,会阻塞住,直到锁释放,锁正常释放后(也就是第一个人执行完f后),第二个人经过再次判断发现done已经被改为1了,此时第二个人放弃执行f ,然后使用了已经被执行过得f产生的结果就很正常了,流程顺理成章。
那么第一个人已经进来了,第二个人为什么也能正常进来?(这里进来指的是进入到doSlow中)
原因就是,在正确实现方式下,会先判断done的值是否为0:
atomic.LoadUint32(&o.done)
来确认是否有往下走的必要,并发时两个人完全有可能均走完了这一步判断都为0(因为还没被改掉),然后进行下一步,所以互斥锁就大显身手了。
综上分析,这是一种思想,不止是这里,相信你在其它地方也会发现此类问题。
怎么样?短短不到数十行代码蕴藏的可是巨大的能量~
要注意,看起来好像很简单,但实际坑还是很多的,当然了,很多都是理解/使用不当自行引入的。
如下,
- once.Do(func(){
-
- once.Do(func(){
-
- // ...
-
- })
-
- })
这种嵌套就相当于连续执行了两遍Lock(),参照源码中的Do实现,这样不死锁才怪呢。
once和init作用有交集、Do在执行时,其内部的func执行后创建了一个不被正常使用的对象,init内又初始化了该类型对象的channel,导致真正要被使用的对象没被使用,而使用了一个为空channel的对象,导致流程阻塞,影响极大,可移步:
可以看到、对象创建、once、channel、并发 千丝万缕,综合起来如不能清晰的明白其中的联系,就会被困扰。
有了上面的详细分析,下面的疑问基本都不是问题了。
来看这一段代码:
- var getOnce sync.Once
- var issucc bool
-
- go func() {
- fmt.Println("A doing...")
- getOnce.Do(func() {
- issucc = true
- fmt.Println("A get success.")
- })
- fmt.Println("A done")
- }()
-
- go func() {
- fmt.Println("B doing...")
- getOnce.Do(func() {
- issucc = true
- fmt.Println("B get success.")
- })
- fmt.Println("B done")
- }()
-
- go func() {
- fmt.Println("C doing...")
- getOnce.Do(func() {
- issucc = true
- fmt.Println("C get success.")
- })
- fmt.Println("C done")
- }()
-
- time.Sleep(time.Second * 3)
如上,A、B、C都想去修改issucc,那么结果如何?
speed running:
- C doing...
- C get success.
- C done
- A doing...
- A done
- B doing...
- B done
如上,C实现了对issucc的修改,而其它两个没有。没错、你没看错,这是正确的,多次尝试,实际上每次都只有一个入口实现对issucc的修改。
和幂等性的出路特征有点像?
先别急,看下一个问题。
看看示例代码:
- var getOnce sync.Once
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("A doing...")
- getOnce.Do(func() {
- fmt.Println("A proc success.")
- })
- fmt.Println("A done")
- }()
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("B doing...")
- getOnce.Do(func() {
- fmt.Println("B proc success.")
- })
- fmt.Println("B done")
- }()
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("C doing...")
- getOnce.Do(func() {
- fmt.Println("C proc success.")
- })
- fmt.Println("C done")
- }()
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("D doing...")
- getOnce.Do(func() {
- fmt.Println("D proc success.")
- })
-
- fmt.Println("D done")
- }()
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("E doing...")
- getOnce.Do(func() {
- fmt.Println("E proc success.")
- })
- fmt.Println("E done")
- }()
-
- go func() {
- time.Sleep(time.Second * 1)
- fmt.Println("F doing...")
- getOnce.Do(func() {
- fmt.Println("F proc success.")
- })
-
- fmt.Println("F done")
- }()
-
- time.Sleep(time.Second * 3)
上述6个goroutine都用了同一个once对象,各自做各自的事情,结果几何?
speed running:
- F doing...
- E doing...
- D doing...
- B doing...
- F proc success.
- F done
- E done
- D done
- B done
- C doing...
- C done
- A doing...
- A done
可以看到和问题1类似的结果,once第一此执行后,后面的不论几次Do再来也不会再执行了。
因此:
对于一个getOnce对象,执行的如果是多个事情,不论是不是相同的事情,只会执行第一个进来的事情。
举个例子:一个once对象,分别被用在了三件不同事情上,如果once第一次执行用在了第一件事上,那么后续的once.DO将不会执行。
看看代码:
- var (
- testDesc *TestDesc
- testDesc2 *TestDesc2
- testDescMap map[string]TestDesc
- onceMany sync.Once
- )
-
- type TestDesc struct {
- Name string
- }
-
- type TestDesc2 struct {
- Name2 string
- }
-
- func newImpl() (map[string]TestDesc, *TestDesc, *TestDesc2) {
- onceMany.Do(func() {
- fmt.Println("once.Do calling...")
- // A
- testDescMap = make(map[string]TestDesc)
-
- // B
- testDesc = new(TestDesc)
-
- // C
- testDesc2 = new(TestDesc2)
- })
-
- fmt.Printf("待返回的testDescMap:%p\n", testDescMap)
- fmt.Printf("待返回的testDesc:%p\n", testDesc)
- fmt.Printf("待返回的testDesc2:%p\n", testDesc2)
- return testDescMap, testDesc, testDesc2
- }
-
- func main() {
- newImpl()
- fmt.Println("1 done")
- newImpl()
- fmt.Println("2 done")
- newImpl()
- fmt.Println("3 done")
- newImpl()
- fmt.Println("4 done")
- newImpl()
- fmt.Println("5 done")
- }
speed running:
- once.Do calling...
- 待返回的testDescMap:0xc000076480
- 待返回的testDesc:0xc00004e230
- 待返回的testDesc2:0xc00004e240
- 1 done
- 待返回的testDescMap:0xc000076480
- 待返回的testDesc:0xc00004e230
- 待返回的testDesc2:0xc00004e240
- 2 done
- 待返回的testDescMap:0xc000076480
- 待返回的testDesc:0xc00004e230
- 待返回的testDesc2:0xc00004e240
- 3 done
- 待返回的testDescMap:0xc000076480
- 待返回的testDesc:0xc00004e230
- 待返回的testDesc2:0xc00004e240
- 4 done
- 待返回的testDescMap:0xc000076480
- 待返回的testDesc:0xc00004e230
- 待返回的testDesc2:0xc00004e240
- 5 done
可以看到Do的f永远只会调用一次,每次使用的对象都是同一个。