Once函数单次调用

认识单例

超超:您好,面试官~

面试官:你好,你平时开发是用 windows 还是 linux 居多?

超超: ̄□ ̄||我平时都是用windows开发的。

面试官:那你知道 windows 的资源管理器只能单开,但是cmd命令行可以开很多个,有想过这是为什么吗?

考点:单例的使用场景优缺点

超超:资源管理器在整个系统运行过程中不会因为不同的任务管理器内容改变而改变,因此为了节省资源全局只需要有一份,这是单例。

单例怎么用

####

面试官:你刚才说到了单例,你知道go里面怎么使用单例吗?

考点:go如何使用单例

超超:( ̄▽ ̄)/ 这个简单,举个例子,婷婷在购物时,android端和web端只有一个指向用户婷婷的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8//婷婷的淘宝客户端和web端都会指向婷婷这一个人
9type Woman struct {
10 name string
11}
12
13var (
14 once sync.Once
15 ting *Woman
16)
17
18func getTing() *Woman {
19 once.Do(func() {
20 ting = new(Woman)
21 ting.name = "tingting"
22 fmt.Println("newtingting")
23 })
24 fmt.Println("gettingting")
25 return ting
26}
27
28func main() {
29 for i := 0; i < 3; i++ {
30 _ = getTing()
31 }
32}

结果

1
2
3
4
1newtingting
2gettingting
3gettingting
4gettingting

源码实现

面试官:那你能说说sync.Once是怎么实现的吗(:举个例子还秀起来了

考点:深入源码,进一步了解**Once**

超超:sync.Once是由Once结构体和其DodoSlow俩个方法实现的

1
2
3
4
5
6
7
8
9
1type Once struct {
2 // done indicates whether the action has been performed.
3 // It is first in the struct because it is used in the hot path.
4 // The hot path is inlined at every call site.
5 // Placing done first allows more compact instructions on some architectures (amd64/x86),
6 // and fewer instructions (to calculate offset) on other architectures.
7 done uint32
8 m Mutex
9}

done是标识位,用来判断方法f是否被执行完,其初始值为0,当f执行结束时,done被设为1。

m做竞态控制,当f第一次执行还未结束时,通过m加锁的方式阻塞其他once.Do执行f

这里有个地方需要特别注意下,once.Do是不可以嵌套使用的,嵌套使用将导致死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 1func (o *Once) Do(f func()) {
2 // Note: Here is an incorrect implementation of Do:
3 //
4 // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
5 // f()
6 // }
7 //
8 // Do guarantees that when it returns, f has finished.
9 // This implementation would not implement that guarantee:
10 // given two simultaneous calls, the winner of the cas would
11 // call f, and the second would return immediately, without
12 // waiting for the first's call to f to complete.
13 // This is why the slow path falls back to a mutex, and why
14 // the atomic.StoreUint32 must be delayed until after f returns.
15
16 if atomic.LoadUint32(&o.done) == 0 {
17 // Outlined slow-path to allow inlining of the fast-path.
18 o.doSlow(f)
19 }
20}
21
22func (o *Once) doSlow(f func()) {
23 o.m.Lock()
24 defer o.m.Unlock()
25 if o.done == 0 {
26 defer atomic.StoreUint32(&o.done, 1)
27 f()
28 }
29}
  • Do()方法

作用:通过原子操作判断o.done,如果o.done==0f未被执行完,进入doSlow(f func()),如果f执行完则退出Do()

入参:无

出参:无

  • doSlow(f func())方法

作用:通过加锁的方式,执行f,并在f执行结束时,将o.done置为1

入参:执行体f,通常为对象的创建或者模块数据加载

出参:无

面试官:你知道atomic.CompareAndSwapUint32(&o.done, 0, 1)的作用是什么吗?

考点:对sync包了解的广度

超超:CompareAndSwapUint32简称CAS,通过原子操作判断当o.done值等于0时,使o.done等于1并返回true,当o.done值不等于0,直接返回false

面试官:很好,那你能说说Do()方法中可以把atomic.LoadUint32直接替换为atomic.CompareAndSwapUint32吗?

考点:多线程思维

超超:这个是不可以的,因为f的执行是需要时间的,如果用CAS可能会导致f创建的对象尚未完成,其他地方就开始调用了。如图所示,A,B俩个协程都调用Once.Do方法,A协程先完成CAS将done值置为了1,导致B协程误以为对象创建完成,继而调用对象方法而出错。

图片

这里doSlow中的o.done == 0判断,也需要注意一下,因为可能会出现A,B俩个协程都进行了LoadUint32判断,并且都是true,如果不进行第二次校验的话,对象会被new俩次

图片

扩展

面试官:看来你对源码sync.Once的实现还比较熟悉,那你知道懒汉模式和饿汉模式吗?

考点:单例创建的延伸

超超:

饿汉模式:是指在程序启动时就进行数据加载,这样避免了数据冲突,也是线程安全的,但是这可能会造成内存浪费。比如在程序启动时就new一个 woman对象加载婷婷相关数据,当需要调用婷婷相关方法时,不用再创建对象。

懒汉模式:是指需要执行对象相关方法时,才主动去加载数据,这样做可以避免内存的浪费,比如当需要调用婷婷相关的方法时,才去new一个 woman对象加载婷婷相关数据,然后调用方法。

面试官:不错,倒是会一点花拳绣腿,那我们开始真正的面试吧。

超超:啊!?

未完待续 ~

以上内容转载自机器铃砍柴刀

-------------本文结束 感谢阅读-------------