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}

~~~

posted on 2022-12-10 13:12  江湖乄夜雨  阅读(112)  评论(0编辑  收藏  举报