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实现单例更加简略,不必殚精竭虑胆怯手抖写出引发线程平安问题的代码了。
如果浏览过程中发现本文存疑或谬误的中央,能够关注公众号留言。如果感觉还能够 帮忙点个在看