【Go语言核心手册13】sync.Once

本文深入探讨Go语言中的sync.Once,它用于确保函数仅执行一次。文章介绍了sync.Once的工作原理,包括其内部的done变量、sync.Mutex和atomic操作确保线程安全。通过源码分析,展示了双重检查和互斥锁的应用,以及与单例模式的相似性。同时,提到了Do方法可能遇到的阻塞问题和重试机制限制,并强调在设计相关流程时需考虑这些问题以避免程序阻塞和功能缺失。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

​往期精选(欢迎转发~~)

当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用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方法的这两个特点来设计与之相关的流程,以避免不必要的程序阻塞和功能缺失。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值