美文网首页
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分析

    Go语言——sync.Once分析 sync.Once表示只执行一次函数。要做到这点,就需要两点: 计数器,统计函...

  • sync.Once

    sync.Once 的使用场景 sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式...

  • go 中的 sync.Once

    sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,sync.Once 仅提供了一...

  • golang 系列:sync.Once 讲解

    sync.Once 介绍 之前提到过 Go 的并发辅助对象:WaitGroup[https://mp.weixin...

  • Go - sync.Once

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

  • 深度剖析Golang sync.Once源码

    目录 什么是sync.Once 如何使用sync.Once 源码分析 什么是sync.Once Once 可以用来...

  • go单例模式之懒汉模式

    可以通过sync包中的sync.Once实现单例模式。 如下是once.go中的Do()方法的源码: 下面是用sy...

  • sync.Once

    利用 sync.once 实现单例 sync.Once 实现 为什么要用defer 来加计数?不直接在后面执行计数...

  • golang sync.Once源代码阅读--让你的初始化只执行

    sync.Once的用处 sync.Once 主要用途执行只需要执行一次的初始化函数,比如验证器的初始,http....

  • GO 语言sync.WaitGroup和sync.Once

    sync.WaitGroup sync.WaitGroup类型(以下简称WaitGroup类型)是开箱即用的,也是...

网友评论

      本文标题:Go - sync.Once

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