美文网首页
Go - sync.Once

Go - sync.Once

作者: kyo1992 | 来源:发表于2021-05-17 18:55 被阅读0次

    设计目的

    Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。

    使用场景

    sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。

    func (o *Once) Do(f func())
    

    因为当且仅当第一次调用 Do 方法的时候参数 f 才会执行,即使第二次、第三次、第 n 次调用时 f 参数的值不一样,也不会被执行。

    例子对比

    以下是用于多个并发goroutine共享一个tcp连接示例,tcp连接对象相当于一个单例资源。

    常规使用互斥锁实现
    package main
    
    import (
        "net"
        "sync"
        "time"
    )
    
    // 使用互斥锁保证线程(goroutine)安全
    var connMu sync.Mutex
    var conn net.Conn
    
    func getConn() net.Conn {
        connMu.Lock()
        defer connMu.Unlock()
    
        // 返回已创建好的连接
        if conn != nil {
            return conn
        }
    
        // 创建连接
        conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
        return conn
    }
    
    func testGetConn() {
        conn := getConn()
        if conn == nil {
            panic("conn is nil")
        }
    }
    
    func main() {
        for i := 0; i < 10; i++ {
            go testGetConn()
        }
        time.Sleep(2 * time.Second)
    }
    
    使用sync.Once实现
    package main
    
    import (
        "net"
        "sync"
        "time"
    )
    
    // 使用互斥锁保证线程(goroutine)安全
    var once sync.Once
    var conn net.Conn
    
    func getConn() {
        // 创建连接
        conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
    }
    
    func testGetConn() {
        once.Do(getConn)
        if conn == nil {
            panic("conn is nil")
        }
    }
    
    func main() {
        for i := 0; i < 10; i++ {
            go testGetConn()
        }
        time.Sleep(2 * time.Second)
    }
    

    sync.Once实现

    type Once struct {
        done uint32
        m    Mutex
    }
    
    func (o *Once) Do(f func()) {
        if atomic.LoadUint32(&o.done) == 0 {
            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 进行初始化,同时利用双检查的机制(double-checking),再次判断 o.done 是否为 0,如果为 0,则是第一次执行,执行完毕后,就将 o.done 设置为 1,然后释放锁。

    即使此时有多个 goroutine 同时进入了 doSlow 方法,因为双检查的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 f。

    这样既保证了并发的 goroutine 会等待 f 完成,而且还不会多次执行 f。

    使用Once容易出错的点

    死锁

    Do 方法会执行一次 f,但是如果 f 中再次调用这个 Once 的 Do 方法的话,就会导致死锁的情况出现。这还不是无限递归的情况,而是的的确确的 Lock 的递归调用导致的死锁。

    func main() {
        var once sync.Once
        once.Do(func() {
            once.Do(func() {
                fmt.Println("初始化")
            })
        })
    }
    

    想要避免这种情况的出现,就不要在 f 参数中调用当前的这个 Once,不管是直接的还是间接的。

    未初始化

    如果 f 方法执行的时候 panic,或者 f 执行初始化资源的时候失败了,这个时候,Once 还是会认为初次执行已经成功了,即使再次调用 Do 方法,也不会再次执行 f。

    解决方法:实现一个类似 Once 的并发原语,既可以返回当前调用 Do 方法是否正确完成,还可以在初始化失败后调用 Do 方法再次尝试初始化,直到初始化成功才不再初始化了。

    
    // 一个功能更加强大的Once
    type Once struct {
        m    sync.Mutex
        done uint32
    }
    // 传入的函数f有返回值error,如果初始化失败,需要返回失败的error
    // Do方法会把这个error返回给调用者
    func (o *Once) Do(f func() error) error {
        if atomic.LoadUint32(&o.done) == 1 { //fast path
            return nil
        }
        return o.slowDo(f)
    }
    // 如果还没有初始化
    func (o *Once) slowDo(f func() error) error {
        o.m.Lock()
        defer o.m.Unlock()
        var err error
        if o.done == 0 { // 双检查,还没有初始化
            err = f()
            if err == nil { // 初始化成功才将标记置为已初始化
                atomic.StoreUint32(&o.done, 1)
            }
        }
        return err
    }
    

    总结

    Once 的应用场景还是很广泛的。一旦遇到只需要初始化一次的场景,尤其是延迟初始化,首先想到的就应该是 Once 并发原语。

    相关文章

      网友评论

          本文标题:Go - sync.Once

          本文链接:https://www.haomeiwen.com/subject/dvlcjltx.html