Go sync.Once

240 阅读3分钟

简单介绍

sync.Once 是 Go 语言标准库 sync 中提供的一个并发原语,用于确保某个函数只会被执行一次,无论有多少个 Goroutine 尝试调用它。

这在 单例模式,资源清理 等场景中非常有用,因为它可以保证代码只执行一次,避免并发时的竞态条件

基本使用

这里给出一个 使用 Once 实现懒汉模式的例子:

  • 单例类声明为不可导出类型 worker,避免被外界直接获取到;
  • 声明一个全局单例变量 var W iWorker,但不进行初始化,留给给外部调用;
  • 暴露一个对外公开的方法 GetWorker(),用于获取这个单例,并且在这个方法被调用时,判断单例是否初始化,倘若没有,则在此时完成初始化工作;
var (
   once sync.Once
   W    iWorker
)

type iWorker interface {
   Work()
}

type worker struct {
}

func (w *worker) Work() {
   fmt.Println("I am working")
}

func newWorker() *worker {
   return &worker{}
}

func GetWorker() iWorker {
   once.Do(func() {
      W = newWorker()
   })
   return W
}

源码分析

sync.Once 数据结构源码截图如下:

type Once struct {
   // done indicates whether the action has been performed.
   // It is first in the struct because it is used in the hot path.
   // The hot path is inlined at every call site.
   // Placing done first allows more compact instructions on some architectures (amd64/386),
   // and fewer instructions (to calculate offset) on other architectures.
   done uint32
   m    Mutex
}

其中 done 是一个整型变量,用来标识 once 保护的函数是否已经被执行过,m 是一个互斥锁,保证并发场景下的安全性。

Once 对外只提供一个方法 once.Do(),本质上是通过 double check 机制,解决并发不安全问题。

func (o *Once) Do(f func()) {
   // Note: Here is an incorrect implementation of Do:
   //
   // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
   //    f()
   // }
   //
   // Do guarantees that when it returns, f has finished.
   // This implementation would not implement that guarantee:
   // given two simultaneous calls, the winner of the cas would
   // call f, and the second would return immediately, without
   // waiting for the first's call to f to complete.
   // This is why the slow path falls back to a mutex, and why
   // the atomic.StoreUint32 must be delayed until after f returns.

   if atomic.LoadUint32(&o.done) == 0 {
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
   }
}

它会先执行一次原子操作,来检查 done 的值是否是 0,如果是 0,则说明该任务没有被执行过,则进一步调用 doSlow() 这个方法执行该任务。

同时,这里解释了一下为什么不能用 atomic.CompareAndSwapUint32(&o.done, 0, 1) 这个操作:假定两个同时调用,那么 CAS 的获胜者将调用 f(), 而另一个将立即返回,无需等待第一个对 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()
   }
}

doSlow() 中,会先加锁,二次检查 Once.done 的值,然后执行方法入参传入的闭包任务函数 f(),最后通过 deferOnce.done 标记为 1,保证全局只执行一次闭包任务函数。

最后结合之前给出的例子,梳理一下函数的执行流程

image.png

注意事项

不要在 Do 的 f() 中嵌套调用 Do,因为 sync.Mutex 是一个不可重入锁,第二个 Do方法会一直等待 doSlow() 中锁的释放导致发生了死锁,如下示例

func TestOnce(t *testing.T) {
   o := &sync.Once{}
   o.Do(func() {
      o.Do(func() {
         t.Log("do------>")
      })
   })
}

// fatal error: all goroutines are asleep - deadlock!