golang源码学习之sync.Once

332 阅读2分钟

一、核心数据结构

// 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、可以用来实现单例