共计 2322 个字符,预计需要花费 6 分钟才能阅读完成。
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 实现单例更加简略,不必殚精竭虑胆怯手抖写出引发线程平安问题的代码了。
如果浏览过程中发现本文存疑或谬误的中央,能够关注公众号留言。如果感觉还能够 帮忙点个在看😁