乐趣区

关于golang:手摸手Go-单例模式与syncOnce

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.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 实例。

`// 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 实现单例更加简略,不必殚精竭虑胆怯手抖写出引发线程平安问题的代码了。

如果浏览过程中发现本文存疑或谬误的中央,能够关注公众号留言。如果感觉还能够 帮忙点个在看😁

退出移动版