手摸手Go 单例模式与sync.Once

I leave uncultivated today, was precisely yesterday perishes tomorrow which person of the body implored。

图片

单例模式做为一个较为常见的设计模式,他的定义也很简单,将类的实例化限制为一个单个实例。在Java的世界里,你可能须要从懒汉模式双重检查锁模式饿汉模式静态内部类枚举等方式中选择一种手动撸一遍代码,可是他们操做起来很容易一不当心就会出现bug。而在Go里,内建提供了保证操做只会被执行一次的sync.Once,操做起来及其简单。web

基本使用

在开发过程当中须要单例模式的场景比较常见,好比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.Mutexatomic包的原子操做来完成。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实例。ui

`// 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的实现atom

`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实现单例更加简单,不用殚精竭虑惧怕手抖写出引起线程安全问题的代码了。

若是阅读过程当中发现本文存疑或错误的地方,能够关注公众号留言。若是以为还能够 帮忙点个在看😁

图片

相关文章
相关标签/搜索