I leave uncultivated today, was precisely yesterday perishes tomorrow which person of the body implored。
单例模式作为一个较为常见的设计模式,他的定义也很简单,将类的实例化限制为一个单个实例
。在Java的世界里,你可能需要从懒汉模式
、双重检查锁模式
、饿汉模式
、静态内部类
、枚举
等方式中选择一种手动撸一遍代码,但是他们操作起来很容易一不小心就会出现bug。而在Go里,内建提供了保证操作只会被执行一次的sync.Once
,操作起来及其简单。
基本使用
在开发过程中需要单例模式的场景比较常见,比如web开发过程中,不可避免的需要跟DB打交道,而DB管理器初始化通常需要保证有且仅发生一次。那么使用sync.Once
实现起来就比较简单了。
`var manager *DBManager`
`var once sync.Once`
`func GetDBManager()*DBManager{`
`once.DO(func(){`
`manager = &DBManager{}`
`manager.initDB(config)`
`})`
`return manager`
`}`
可以看到仅仅需要once.DO(func(){...})
即可, 开发者只需要关注自己的初始化程序即可,单例由sync.Once
来保证,极大降低了开发者的心智负担。
sync.Once源码分析
数据结构
sync.Once
结构也比较简单,只有一个uint32
字段和一个互斥锁Mutex
。
`// 一旦使用不允许被拷贝`
`type Once struct {`
`// done表示当前的操作是否已经被执行 0表示还没有 1表示已经执行`
`// done属性放在结构体的第一位,是因为它在hot path中使用`
`// hot path在每个调用点会被内联。`
`// 将done放在结构体首位,像amd64/386等架构上可以允许更多的压缩指令`
`// 并且在其他架构上更少的指令去计算偏移量`
`done uint32`
`m Mutex`
`}`
sync.Once
的核心原理,是利用sync.Mutex
和atomic
包的原子操作来完成。done
表示是否成功完成一次执行。存在两个状态:
- 0 表示当前
sync.Once
的第一次DO
操作尚未成功 - 1 表示当前
sync.Once
的第一次DO
操作已经完成
每次DO
方法调用都会去检查done
的值,如果为1则啥也不做;如果为0则进入doSlow
流程,doSlow
很巧妙的先使用sync.Mutex
。这样如果并发场景,只有一个goroutine
会抢到锁执行下去,其他goroutine
则阻塞在锁上,这样的好处是如果拿到锁的那个goroutine
失败,其他阻塞在锁上的goroutine
就是预备队替补上去。确保sync.Once
有且仅成功执行一次的语义。
once flow graph
好了,接下来看源码
操作方法
Do
Do
执行函数f
当且仅当对应sync.Once
实例第一次调用Do
。换句话说,给定var once Once
,如果once.Do(f)
被调用了多次,,尽管f
在每次调用的值均不同,但只有第一次调用会执行f
。如果需要每个函数都执行,则需要新的sync.Once
实例。
`// Do的作用主要是针对初始化且有且只能执行一次的场景。因为Do直到f返回才返回,`
`// 所以如果f内调用Do则会导致死锁`
`// 如果f执行过程中panic了 那么Do任务f已经执行完毕 未来再次调用不会再执行f`
`func (o *Once) Do(f func()) {`
`if atomic.LoadUint32(&o.done) == 0 {//判断f是否被执行`
`// 可能会存在并发 进入slow-path`
`o.doSlow(f)`
`}`
`}`
注释里提到了一种不正确的Do
的实现
`if atomic.CompareAndSwapUint32(&o.done, 0, 1) {`
`f()`
`}`
这种实现不正确的原因在于,无法保证f()
有且仅执行一次的语义。因为使用直接CAS来解决问题,如果同时有多个goroutine
竞争执行Do
那么是能保证有且仅有一个goroutine
会得到执行机会,其他goroutine
只能默默离开。
但是如果获得执行机会的goroutine
执行失败了,那么以后f()
就在也没有执行机会了。
那么我们来看看官方的实现方式
`func (o *Once) doSlow(f func()) {`
`o.m.Lock()`
`defer o.m.Unlock()`
`if o.done == 0 {//二次判断f是否已经被执行`
`defer atomic.StoreUint32(&o.done, 1)`
`f()`
`}`
`}`
官方的做法就是如果多个goroutine
都来竞争Do
,那么先让一个goroutine
拿到sync.Mutex
的锁,其他的goroutine
先不着急让他们直接返回,而是都先阻塞在sync.Mutex
上。如果那个拿到锁的goroutine
很不幸执行f()
失败了,那么defer o.m.Unlock()
操作会立刻唤醒阻塞的goroutine
接着尝试执行直到成功为止。执行成功后通过defer atomic.StoreUint32(&o.done, 1)
来将执行f()
的大门给关闭上。
总结
有了sync.Once
,相比Java或者Python实现单例更加简单,不用殚精竭虑害怕手抖写出引发线程安全问题的代码了。
如果阅读过程中发现本文存疑或错误的地方,可以关注公众号留言。如果觉得还可以 帮忙点个在看😁
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。