考虑下面这段代码会打印出什么:

var count int

increment := func() {
    count++
}

var once sync.Once

var wg sync.WaitGroup

wg.Add(100)
for i := 0; i < 100; i++ {
    go func() {
        defer wg.Done()
        once.Do(increment)
    }()
}

wg.Wait()
fmt.Printf("Count is %d\n", count)

你肯定已经注意到了sync.Once类型的变量,没错,这段代码将打印以下内容:

Count is 1

顾名思义,sync.Once确保了即使在不同的goroutine上,调用Do传入的函数只执行一次。

看起来将多次调用一个函数但执行一次的能力封装并放入标准库是一件奇怪的事情,但事实证明,对这种模式的需求相当频繁。为了好玩,让我们来检查Go的标准库,看看Go本身使用这个原语的频率。 这是一个将执行搜索的grep命令:

grep -ir sync.Once $(go env GOROOT)/src |wc -l

这会输出:

70

关于利用sync.Once有几点需要注意。我们来看看另一个例子。 你认为它会打印什么?

var count int
increment := func() { count++ }
decrement := func() { count-- }

var once sync.Once
once.Do(increment)
once.Do(decrement)

fmt.Printf("Count: %d\n", count)

这会输出:

Count: 1

输出显示1而不是0令人惊讶吗? 这是因为sync.Once只计算Do被调用的次数,而不是调用传入Do的唯一函数的次数。 通过这种方式,sync.Once的副本与被用于调用的函数紧密耦合;我们再次看到sync包内如何在一个紧密的范围内发挥最佳效果。 我建议你通过在一个小的词法块中包装sync.Once的来形式化这种耦合:一个小型函数,或者通过包装在一个类型中。 那么下面这个例子呢? 你认为会发生什么?

var onceA, onceB sync.Once
var initB func()
initA := func() { onceB.Do(initB) }
initB = func() { onceA.Do(initA) } // 1
onceA.Do(initA) // 2
  1. 这里的调用无法执行,直到2被返回。

这段程序会发生死锁,因为在1处对Do的调用不会执行直到2执行完毕,而2处无法结束执行——这是一个标准的死锁示例。这可能有点反直觉,看起来好像我们正在使用sync.Once来防止多个初始化。有时候程序出现死锁正是由于逻辑中出现了循环引用。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng