本文共 4265 字,大约阅读时间需要 14 分钟。
1、NSQD 启动过程中对sync.Once的使用
源码路径:apps/nsqd/main.go
核心结构体:
type program struct { once sync.Once nsqd *nsqd.NSQD}
在使用Once的时候,可以尝试采用此种结构,将值和Once封装成一个新的数据机构,提供只初始化一次或只结束一次的值。
例如,nsqd中,在Stop()方法里面,利用sync.Once保证,对nsqd执行且仅仅执行一次退出操作
func (p *program) Stop() error { p.once.Do(func() { p.nsqd.Exit() }) return nil}
2、sync.Once探索
(1)什么是sync.Once
Once就是一个结构体,且此结构体只有两个属性done uint32 和 m Mutex.
Once可以用来执行且仅仅执行一次动作
并且通过源码注释可以发现,done 在此结构体的第一个位置是为了能够在 hot path 中被用到。
hot path 能够在函数每次调用时被内联调用。
关于Go语言的inline内联调用可以查看此文章:
(2)Once 的使用场景
sync.Once只暴露了一个方法Do,你可以多次调用Do 方法,但是只有第一次调用Do方法时f参数才会执行,这里的f参数是一个无参无返回值的函数。
// Do calls the function f if and only if Do is being called for the// first time for this instance of Once. In other words, given// var once Once// if once.Do(f) is called multiple times, only the first call will invoke f,// even if f has a different value in each invocation. A new instance of// Once is required for each function to execute.func (o *Once) Do(f func())
当且仅当第一次调用Do 方法的时候参数f才会执行,即使第一次、第二次、第三次、第n次调用时f参数的值不一样,也不会被执行。例如:
package mainimport ( "fmt" "sync")func main() { var once sync.Once f1 := func() { fmt.Printf("in func1") } once.Do(f1) // 打印出 in func1 f2 := func() { fmt.Printf("in func2") } once.Do(f2) // 无输出 f3 := func() { fmt.Printf("in func3") } once.Do(f3) //无输出}
虽然f1、f2和f3是不同的函数,但是第二个函数f2 和 第三个函数f3不会执行。
通过sync.Once的源码注释
// Do is intended for initialization that must be run exactly once. Since f// is niladic, it may be necessary to use a function literal to capture the// arguments to a function to be invoked by Do:// config.once.Do(func() { config.init(filename) })func (o *Once) Do(f func())
可以看出因为这里的f 是一个无参数无返回值的函数,所有可以通过 闭包 的方式引用外面的参数。比如:
var addr = "baidu.com"var conn net.Connvar err erroronce.Do(func(){ conn,err = net.Dial("tcp",addr)})
在实际的使用过程中,绝大数情况下,你会使用闭包的方式去初始化外部的一个资源。
例如在math/big/sqrt.go中,实现的一个数据结构,它通过Once封装了一个只初始化一次的值。
//值是3.0或0.0的一个数据结构var threeOnce struct { sync.Once v *Float}func three() *Float { threeOnce.Do(func() { //使用Once初始化 threeOnce.v = NewFloat(3.0) }) //返回此数据结构的值,如果还没有初始化为3.0,则进行初始化 return threeOnce.v}
因此可以总结出sync.Once这个并发原语要解决的问题和使用的场景为:用来初始化单例资源、并发访问时只需要初始化一次的共享资源或只需要执行一次的退出操作。
(3)Once 的实现
标准库中的实现为:
type Once struct { done uint32 m Mutex}func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { // fast-path // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) }}func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() //双重检查 if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) //不会内联 f() }}
所以,通过标准库的源码实现可以学习到,一个正确的 Once实现要使用一个互斥锁,这样初始化的时候,如果有并发的goroutine,就会执行doSlow方法。互斥锁的机制保证只有一个goroutine进行初始化。同时在doSlow方法中利用双检查机制(double-checking),再次判断o.done的值是否为0,如果为0,则是第一次执行,执行完成后,通过 defer 将o.done设置为1,最后释放锁。
如果此时,有更多的goroutine同时执行doSlow方法,因为双检查机制,后续的goroutine只会看到o.done的值为1,不会再次去执行f函数。
这种从XXX方法中,单独抽出XXXSlow方法去执行的好处是,分离固定的静态逻辑和动态逻辑,使得固定静态的逻辑能够被内联调用,提高执行效率。
在源码注释中,有一个错误的实现示例:
// Note: Here is an incorrect implementation of Do://// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {// f()// }//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first's call to f to complete.// This is why the slow path falls back to a mutex, and why// the atomic.StoreUint32 must be delayed until after f returns
这个实现会有一个很大的问题,如果有两个goroutine同时执行,先抢到的goroutine将会调用f,则第二个则会立即返回,不会去等第一个调用f完成。如果f逻辑很复杂,执行效率很慢的话,后续调用Do方法的goroutine虽然看到done已经设置为执行过了,但是获取某些初始化资源的时候,可能会得到空的资源,因为f还没有执行完。
所以,正确的实现方式就是利用互斥锁+双检查机制。
(4)Once 的错误使用方式
在源码注释中,有进行说明:
// Because no call to Do returns until the one call to f returns, if f causes// Do to be called, it will deadlock.//// If f panics, Do considers it to have returned; future calls of Do return// without calling f.
第一种情况:死锁
因为只有对f的调用返回时,对Do的调用才会返回,如果f中,有引起再次调用这个Once的Do方法的话,会导致死锁情况的出现。这是Lock的递归调用导致的死锁。例如:
func main() { var once sync.Once once.Do(func() { once.Do(func() { fmt.Println("Init...") }) })}
第二种情况:异常导致初始化资源不完整
如果 f 方法执行的时候 panic,或者 f 执行初始化资源的时候失败了,Once 还是会认为初次执行已经成功了,即使再次调用 Do 方法,也不会再次执行 f。
转载地址:http://dwmvz.baihongyu.com/