Go 同步原语 sync.Once 详解

325 阅读1分钟

概述

Go语言标准库中的 sync.Once 可以保证 Go 语言程序运行期间某段代码只会执行一次。

下面我们看一段代码:

func main(){
    o := &sync.Once{}
    for i := 0; i < 10; i++ {
        o.Do(func () {
            fmt.Println("only once")
        })
    }
}

运行结果:

only once

这里结果只输出一次,所以显而易见,sync.Once 在全局执行时,只会执行一次。

结构体

每一个 sync.Once 结构体中都只包含一个用于标识代码是否执行过的 done,以及一个互斥锁 sync.Mutex

type Once struct {
    done uint32
    m    Mutex
}

接口

sync.Once.Dosync.Once 结构体对外唯一暴露的方法,该方法会接收一个入参为空的函数:

  • 如果传入的函数已经执行过,会直接返回
  • 如果传入的函数没有执行过,会调用 sync.Once.doSlow 执行传入的函数
func (o *Once) Do (f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        o.doneSlow(f)
    }
}

func (o *Once) doneSlow (f func ()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

sync.Once.doSlow 的核心逻辑

  • 为当前 Goroutine 获取互斥锁
  • 执行传入的无入参函数
  • 运行延迟函数调用,将成员变量 done 更新为 1
  • sync.Once 会通过成员变量 done 确保函数不会执行第二次

小结

作为用于保证函数执行次数的 sync.Once 结构体,它使用互斥锁和 sync/atomic 包提供的方法实现了某个函数在程序运行期间只能执行一次的语义。

使用该结构体时需要注意以下问题:

  • sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic
  • 两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调用传入的函数