Golang标准库中的sync.Once是一个线程安全的用于执行一次性操作的对象。对于同一个sync.Once对象,在第一次执行其Do方法时将调用该方法的参数函数,而完成后再次调用Do方法也不会再执行该参数函数。

例如下列实例中,将在循环中新建一个goroutine并调用once.Do方法,并将会打印Only once文本的onceBody方法作为参数传递至once.Do方法中。随后通过channel确保所有goroutine都执行完成:

package main

import (
	"fmt"
	"sync"
)

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文本。

$ go run once.go
Only once

sync.Once源码分析

在上文中我们已经知道,sync.Once是通过一个对象实现的,我就先来看看它的属性都包括哪些:

type Once struct {
  done uint32
  m    Mutex
}

可以看到,Once结构体只包含了两个属性,分别为uint32类型的done以及sync.Mutex类型的m,它们分别为标记是否已经执行过的标志以及执行时所用的互斥锁。

该结构体将done属性放置于结构体中的第一个位置,是利用了一种名为hot path的优化。其在AMD64/368架构下CPU将使用更为紧凑的指令,而在其它架构下也将减少需要的指令数量(sync.Once实例指针地址即其done属性的地址,避免了计算偏移地址)。

除了结构体外,sync.Once还包括了一个公开的方法Do

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 {
    o.doSlow(f)
  }
}

Once.Do方法的实现非常简单,通过atomic.LoadUint32获取Once实例的done属性值。若done值为0时,表示函数f未被调用过或正运行中且未结束,则将调用doSlow方法;若done值为1时,表示函数f已经调用且完成,则直接返回。

这里使用了原子操作方法atomic.LoadUint32而不是直接将o.done进行比较,也是为了避免并发状态下错误地判断执行状态,产生不必要的锁操作带来的时间开销。

另外,我们可以在代码的注释文档中可以看到开发者标记的一种通过atomic.CompareAndSwapUint32的错误实现。使用atomic.CompareAndSwapUint32实现时,若有两次调用同时进行时,竞争成功的调用将进入函数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.doSlow方法的实现使用了传统的互斥锁Mutex操作,在执行时即调用o.m.Lock方法获得锁,然后再继续判断是否已经完成并调用f函数。可以看到,在获得锁后还需要对o.done的值进行一次判断,避免了f函数被重复调用。

最后,在退出doSlow方法时还需要对获取的锁进行释放,若进入到f函数的调用则需要更改o.done属性值。

使用sync.Once实现单例模式示例

sync.Once可被用于单例模式的实现中。在不使用sync.Once的情况下为了实现一个线程安全的单例,我们通常会使用sync.Mutex对获取单例的方法进行加锁,例如下面的示例:

var ins *SingletonType
var locker sync.Mutex

func GetSingleton() {
  locker.Lock()
  defer locker.Unlock()

  if ins == nil {
    ins = &SingletonType{}
  }

  return ins
}

而在使用sync.Once的情况下,我们只需要在单例未初始化的情况下调用once.Do进行初始化操作即可,而无需每次都进行互斥锁的操作,减少了锁操作消耗的时间:

var ins *SingletonType
var once sync.Once

func GetSingleton() {
  once.Do(func () {
    ins = &SingletonType{}
  })

  return ins
}

使用sync.Mutex进行互斥锁的操作,是一个相对缓慢的过程。对比于sync.Mutex的实现方法,使用sync.Once能有效地提高程序的性能。