一、核心数据结构
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
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)和互斥锁(mutex)
二、核心方法
1、Do(f func())方法
func (o *Once) Do(f func()) {
// 原子操作,加载标志位,如果标志位为0,需要第一次执行f()
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
2、 doSlow(f func()) 方法
func (o *Once) doSlow(f func()) {
// 互斥锁
o.m.Lock()
defer o.m.Unlock()
// double check
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
这两个核心函数比较简单,当两个goroutine并发访问Do的时候,先判断标志位done,为0表示还未操作,继续操作,此时加上一个互斥锁,让goroutine竞争之后有序进入,此时再次判断标志位done,如果已经有goroutine执行了f(),那么done=1,直接返回,否则执行f(),然后设置done=1
三、总结
1、由于有互斥锁,所以所有goroutine都会等到f()函数执行之后才会继续执行后续逻辑,这里面存在一个happen before的逻辑,f()执行完成一定before Do函数返回
2、双重检验(double check),采用这个机制保证,只会有一个goroutine执行f()
3、官方示范
// 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.
sync.Do的一个准则:Do保证当它返回时,f()已经完成。 CAS实现不会实施这种保证:假设两个goroutine同时调用,竞争胜利的goroutine会执行f,失败的第二个会立即返回,而不是正在等待第一个对f的调用完成。这就是为什么慢路径会退回到互斥锁,为什么这个atomic.StoreUint32必须推迟到f之后
4、可以用来实现单例