目录
前言
Go 版本 1.16
如果本文对你有帮助,给个赞吧;
喜欢本文就收藏一下吧;
有问题欢迎评论留言,基本都会回。
1. sync.Once 简介
sync.Once 是 Go 语言实现的一种对象,用来保证某种行为只会被执行一次。
它只提供一个 API:
func (o *Once) Do(f func())
无论调用多少次 Do
,都只有第一次调用生效。
2. sync.Once 源码解析
// 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
字段用来判断某行为action
是否已进行,因为它hot path
中被使用,放在结构体的第一个字段能够减少机器指令。Once
用过了就不要再复制产生副本。
2.1 为什么 done 作为第一个字段
hot path
:程序频繁执行的一些指令。
在源码中 done
字段频繁被访问(后面源码分析会讲到),所以它处在 hot path
上。
那为什么作为第一个字段就能减少 CPU 指令、提高性能呢?
因为结构体第一个字段的地址和结构体的地址是一样的,要访问第一个字段直接对结构体指针进行解引用即可,而访问后面的字段就要计算偏移量(前面字段所占字节空间 + 是否进行了内存对齐),就会增加 CPU 指令。
2.2 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 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
源码注释中给出了一种 Do
的错误实现方式:使用 CAS 操作判断 f 是否已经执行,如果没有则执行,否则不执行。
看起来没什么问题,源码给出解释:Do
应该保证当自己返回时,f
已经执行完毕。当同时调用两次 Do
时,竞争成功者将原子地把 done
从 0
改为 1
,失败者再进行 CAS 操作时发现不满足条件将直接返回,没有等成功者将 f
执行完。
这也就是为什么源码实现要用到互斥锁 mutex
以及为什么 atomic.StoreUint32
操作要等 f
返回后再执行(见下文 doSlow 分析)。
func (o *Once) doSlow(f func()) {
o.m.Lock() // 上锁
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
使用 defer 可以保证 f
先执行完,在 doSlow
返回时才执行 atomic.StoreUint32(&o.done, 1)
,当然 o.m.Unlock()
也是在 doSlow 返回时执行。
注:defer 的执行顺序是后进先出,也就是最后 defer 的函数,在返回时最先被执行。
看完源码,上来就先原子加载 done
,上锁后还访问一次 done
,因此说 done
在 hot path
上(填坑)。
思考:为什么 atomic.StoreUint32(&o.done, 1)
要用 defer 关键字,而不是直接写在 f()
后面呢?
因为 Once
本身的语义就是对外保证你传进来 f
执行过一次,若 f
在执行过程中 panic
了,会导致 Do
也直接退出,但是退出前会把所有的 defer 都执行完,保证了 f
执行过一次。若放在 f()
后面,当 f
发生 panic
之后,done
就不能置为 1
。
2.3 其他重要细节
-
问:对于源码中举例的错误实现方式,并发环境下,
Do
可能被多次调用,竞争失败者并没有等待成功者的f
执行完就返回了,那么源码是怎么保证的呢?
答:源码通过互斥锁保证的,竞争失败者由于没有获得互斥锁会阻塞在o.m.Lock()
不会立即返回,只有当成功者执行完f
并释放锁之后,失败者们才能依次获得锁,但由于此时done
已经被成功者改为1
,失败者们就都不会执行f
了。 -
sync.Once
是线程安全的,互斥锁保证只有一个线程能够修改done
的值。 -
once.Do(f func())
方法不能嵌套,若f
在执行过程中也会调用once.Do
,会导致死锁。原因很简单,f
要获得锁才能执行,而外层的Do
已经获得并等f
执行完才能释放锁(我在等你,你在等我,谁也不肯先放手= =)。
3. sync.Once 的应用场景
- 配合单例模式,让实例只初始化一次。
var instance int // 模拟单例模式的实例
var once sync.Once
func getInstance() int {
once.Do(func () {
instance = 2
})
return instance
}
Do
只执行一次的特性实现来单例模式的懒汉式加载。
- 初始化项目的配置,与
init
函数类似。因为init
函数只会在package
被加载时执行一次,若迟迟未被加载,则会浪费内存,所以可以使用Once
初始化配置。