Golang sync.Once

这里填写标题

1. Golang sync.Once

当一个函数不希望程序在一开始的时候就被执行的时候, 我们可以使用 sync.Once。Once 类型的 Do 方法只接受一个参数, 这个参数的类型必须是 func(), 即: 无参数声明和结果声明的函数。该方法的功能并不是对每一种参数函数都只执行一次, 而是只执行"首次被调用时传入的"那个函数, 并且之后不会再执行任何参数函数。所以, 如果你有多个只需要执行一次的函数, 那么就应该为它们中的每一个都分配一个 sync.Once 类型的值(以下简称 Once 值), 看个示例:

func main() {    var once sync.Once    onceBody := func() {        fmt.Println("Only once")    }    done := make(chan bool)    for i := 0; i < 10; i++ {        go func() {            once.Do(onceBody)            done <- true    }()    }    for i := 0; i < 10; i++ {        <-done    }}// 输出 Only once

sync.Once 使用变量 done 来记录函数的执行状态, 使用 sync.Mutex 和 sync.atomic 来保证线程安全的读 done, 我们看一下源码:

type Once struct {    m    Mutex    done uint32}func (o *Once) Do(f func()) {    if atomic.LoadUint32(&o.done) == 1 {        return    }    o.m.Lock()    defer o.m.Unlock()    if o.done == 0 {        defer atomic.StoreUint32(&o.done, 1)        f()    }}

你可能会问, 既然 done 字段的值不是 0 就是 1, 那为什么还要使用需要四个字节的 uint32 类型呢? 原因很简单, 因为对它的操作必须是"原子"的, Do 方法在一开始就会通过调用 atomic.LoadUint32 函数来获取该字段的值, 并且一旦发现该值为 1, 就会直接返回。

这也初步保证了"Do 方法, 只会执行首次被调用时传入的函数"。不过, 单凭这样一个判断的保证是不够的, 因为, 如果有两个 goroutine 都调用了同一个新的 Once 值的 Do 方法, 并且几乎同时执行到了其中的这个条件判断代码, 那么它们就都会因判断结果为 false, 而继续执行 Do 方法中剩余的代码。在这个条件判断之后, Do 方法会立即锁定其所属值中的那个 sync.Mutex 类型的字段 m。然后, 它会在临界区中再次检查 done 字段的值, 并且仅在条件满足时, 才会去调用参数函数, 以及用原子操作把 done 的值变为 1。如果你熟悉 GoF 设计模式中的单例模式的话, 那么肯定能看出来, 这个 Do 方法的实现方式, 与那个单例模式有很多相似之处。它们都会先在临界区之外, 判断一次关键条件, 若条件不满足则立即返回, 这通常被称为"快速失败路径"。如果条件满足, 那么到了临界区中还要再对关键条件进行一次判断, 这主要是为了更加严谨, 这两次条件判断常被统称为(跨临界区的)“双重检查”。由于进入临界区之前, 肯定要锁定保护它的互斥锁 m, 显然会降低代码的执行速度, 所以其中的第二次条件判断, 以及后续的操作就被称为"慢路径"或者"常规路径"。别看 Do 方法中的代码不多, 但它却应用了一个很经典的编程范式。下面我们再看看 Do 方法的两个特点:

  • 阻塞问题: 由于 Do 方法只会在参数函数执行结束之后把 done 字段的值变为 1, 因此, 如果参数函数的执行需要很长时间或者根本就不会结束(比如执行一些守护任务), 那么就有可能会导致相关 goroutine 的同时阻塞。例如, 有多个 goroutine 并发地调用了同一个 Once 值的 Do 方法, 并且传入的函数都会一直执行而不结束。那么, 这些 goroutine 就都会因调用了这个 Do 方法而阻塞。因为, 除了那个抢先执行了参数函数的 goroutine 之外, 其他的 goroutine 都会被阻塞在锁定该 Once 值的互斥锁 m 的那行代码上。
  • 重试机制问题: Do 方法在参数函数执行结束后, 对 done 字段的赋值用的是原子操作, 并且, 这一操作是被挂在 defer 语句中的。因此, 不论参数函数的执行会以怎样的方式结束, done 字段的值都会变为 1。也就是说, 即使这个参数函数没有执行成功(比如引发了一个 panic), 我们也无法使用同一个 Once 值重新执行它了。所以, 如果你需要为参数函数的执行设定重试机制, 那么就要考虑 Once 值的适时替换问题。

在很多时候, 我们需要依据 Do 方法的这两个特点来设计与之相关的流程, 以避免不必要的程序阻塞和功能缺失。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值