Go sync.Once 原理

sorcererxw /

sync.Once 用于保证一段任务执行 exactly 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 的工作流程:

  • 通过 atomic 来检查 done 判断是否已经被执行了
  • 通过 Mutex 保证同一时刻只有一个协程进入执行
  • 检查一遍 done 是否被改写
  • 执行并更新 done 值

整个流程其实就是双检锁单例的实现,在锁的基础上引入了 double check,所以有下面几个点值得注意:

  • 为什么要设置多次的检查?

    观察一下 doSlow ,这本身就是一个完整且可以独立的工作的流程,但是它的问题在于它太慢了,每一次检查都需要竞争锁。所以为了提高性能,通过前置检查 atomic 来快速判断是否已经处理完成。

  • 既然前面已经检查过 done 的值了,为什么在获取到锁执行前需要再检查一遍?

    因为整个流程并非原子操作,在等待互斥锁的时候,前面的协程可能已经改写了 done 值,后面再放行进入的协程需要保证 done 没有被改写过。