sync.Once的基本使用以及拓展
基本的单例模式
之前总结过博客:https://www.cnblogs.com/paulwhw/p/15450657.html#_label2
看一下Once的源码
type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { // 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() } }
为什么要这样做呢?大家想一个问题:如果参数 f 执行很慢的话,后续调用 Do 方法的 goroutine 虽然看到 done 已经设置为执行过了,但是获取某些初始化资源的时候可能会得到空的资源,因为 f 还没有执行完。
所以,一个正确的 Once 实现要使用一个互斥锁,这样初始化的时候如果有并发的 goroutine,就会进入doSlow 方法。互斥锁的机制保证只有一个 goroutine 进行初始化,同时利用双检查的机制(double-checking),再次判断 o.done 是否为 0,如果为 0,则是第一次执行,执行完毕后,就将 o.done 设置为 1,然后释放锁。
问题1 解决第一次初始化出错的问题
如果 f 方法执行的时候 panic,或者 f 执行初始化资源的时候失败了,这个时候,Once 还是会认为初次执行已经成功了,即使再次调用 Do 方法,也不会再次执行 f。
实际上此时资源并没有正确的初始化,后面的协裎也不会再初始化资源了,会导致后续的逻辑出现问题:
package a_syncOnce import ( "fmt" "sync" "testing" ) type Config struct { Server string Port int64 } var ( config *Config once sync.Once ) func initConfig() { defer func() { if err := recover(); err != nil { fmt.Println("err: ", err) } }() fmt.Println("init config......") panic("init Error") // 模拟初始化配置发生异常!!! config = &Config{Server: "127.0.0.1", Port: 9999} } // 获取配置 func ReadConfig(i int) *Config { fmt.Println("i>>> ", i) // initConfig() once.Do(initConfig) return config } func TestSingleTon(t *testing.T) { // 实际中可能会有多个协程同时去调用ReadConfig wait := sync.WaitGroup{} for i := 0; i < 10; i++ { wait.Add(1) go func(i int) { defer wait.Done() _ = ReadConfig(i) }(i) } wait.Wait() fmt.Println("config: ", config) // nil —— 得到的配置是个nil,其实后面的逻辑不能再用了 }
`我们可以自己实现一个类似 Once 的并发原语,`既可以返回当前调用 Do 方法是否正确完成,还可以在初始化失败后调用 Do 方法再次尝试初始化,直到初始化成功才不再初始化了。
package a_syncOnce import ( "errors" "fmt" "math/rand" "sync" "sync/atomic" "testing" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } type Config struct { Server string Port int64 } // 自定义一个Once type OncePower struct { sync.Mutex done uint32 } // 传入的函数fun有返回值error,如果初始化失败,需要返回失败的error // Do方法会把这个error返回给调用者 func (o *OncePower) Do(fun func() error) error { // 初始化过了 if atomic.LoadUint32(&o.done) == 1 { return nil } return o.slowDo(fun) } // 还未初始化 func (o *OncePower) slowDo(fun func() error) error { o.Lock() defer o.Unlock() if o.done == 0 { // 双检查 err := fun() if err != nil { return err } // 初始化成功才将标记置为已初始化 atomic.StoreUint32(&o.done, 1) } return nil } var ( config *Config once OncePower ) func initConfig() error { // 用随机数模拟错误 randInt := rand.Intn(5) if randInt > 1 { return errors.New("初始化错误!") } fmt.Printf("init config.....\n") config = &Config{Server: "127.0.0.1", Port: 9999} return nil } // 获取配置 func ReadConfig(i int) *Config { // initConfig() err := once.Do(initConfig) if err != nil { fmt.Printf("%d 出错了 %s \n", i, err.Error()) } return config } func TestSingleTon(t *testing.T) { // 实际中可能会有多个协程同时去调用ReadConfig wait := sync.WaitGroup{} for i := 0; i < 10; i++ { wait.Add(1) go func(i int) { defer wait.Done() _ = ReadConfig(i) }(i) } wait.Wait() fmt.Println("config: ", config) // 得到的配置是个nil,其实后面的逻辑不能再用了 }
结果:
8 出错了 初始化错误! 9 出错了 初始化错误! 5 出错了 初始化错误! init config..... 4 出错了 初始化错误! config: &{127.0.0.1 9999}
可以看到:即使有协裎出现错误,也会有其他协裎做补救,最终还是能把资源正确的初始化~
问题2 如何查询是否初始化过
还有个问题,`我们怎么查询是否初始化过呢?`
目前的 Once 实现可以保证你调用任意次数的 once.Do 方法,它只会执行这个方法一次。
但是,有时候我们需要打一个标记。如果初始化后我们就去执行其它的操作,标准库的 Once 并不会告诉你是否初始化完成了,只是让你放心大胆地去执行 Do 方法,所以,`你还需要一个辅助变量,自己去检查是否初始化过了`,比如通过下面的代码中的 inited 字段:
package a_syncOnce import ( "errors" "fmt" "math/rand" "sync" "sync/atomic" "testing" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } type Config struct { Server string Port int64 } // 自定义一个Once type OncePower struct { sync.Mutex done uint32 } // 传入的函数fun有返回值error,如果初始化失败,需要返回失败的error // Do方法会把这个error返回给调用者 func (o *OncePower) Do(fun func() error) error { // 初始化过了 if atomic.LoadUint32(&o.done) == 1 { return nil } return o.slowDo(fun) } // 还未初始化 func (o *OncePower) slowDo(fun func() error) error { o.Lock() defer o.Unlock() if o.done == 0 { // 双检查 err := fun() if err != nil { return err } // 初始化成功才将标记置为已初始化 atomic.StoreUint32(&o.done, 1) } return nil } var ( config *Config once OncePower initedConfig uint32 ) func initConfig() error { // 用随机数模拟错误 randInt := rand.Intn(10) if randInt > 1 { return errors.New("初始化错误!") } fmt.Printf("init config.....\n") config = &Config{Server: "127.0.0.1", Port: 9999} // 初始化成功,给一个成功的标志 if atomic.LoadUint32(&initedConfig) == 0 { atomic.StoreUint32(&initedConfig, 1) } return nil } // 获取配置 func ReadConfig(i int) *Config { // initConfig() err := once.Do(initConfig) if err != nil { fmt.Printf("%d 出错了 %s \n", i, err.Error()) } // 初始化成功了,就可以大胆用config了 if atomic.LoadUint32(&initedConfig) == 1 { fmt.Println(i, "放心大胆的使用Config!", config) } return config } func TestSingleTon(t *testing.T) { // 实际中可能会有多个协程同时去调用ReadConfig wait := sync.WaitGroup{} for i := 0; i < 10; i++ { wait.Add(1) go func(i int) { defer wait.Done() _ = ReadConfig(i) }(i) } wait.Wait() fmt.Println("config: ", config) // 得到的配置是个nil,其实后面的逻辑不能再用了 }
输出如下:
0 出错了 初始化错误! init config..... 4 放心大胆的使用Config! &{127.0.0.1 9999} 5 放心大胆的使用Config! &{127.0.0.1 9999} 6 放心大胆的使用Config! &{127.0.0.1 9999} 7 放心大胆的使用Config! &{127.0.0.1 9999} 9 放心大胆的使用Config! &{127.0.0.1 9999} 8 放心大胆的使用Config! &{127.0.0.1 9999} 2 放心大胆的使用Config! &{127.0.0.1 9999} 1 放心大胆的使用Config! &{127.0.0.1 9999} 3 放心大胆的使用Config! &{127.0.0.1 9999}
~~~