sync.Once
利用 sync.once 实现单例
var s *SomeObject // 全局变量(我们只希望创建一个)
var once sync.Once // 定义一个 once 变量
func GetInstance() *SomeObject {
once.Do(func() {
s = &SomeObject{} // 创建一个对象,赋值指针给全局变量
})
return s
}
sync.Once 实现
type Once struct {
done uint32 // 保证变量仅被初始化一次,需要有个标志来判断变量是否已初始化过,若没有则需要初始化
m Mutex // 线程安全,支持并发,无疑需要互斥锁来实现
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f) // 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。
}
}
func (o *Once) doSlow(f func()) {
// 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
// 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
为什么要用defer 来加计数?不直接在后面执行计数
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
因为处理不了 panic 的异常,举个例子:
如果不用 defer ,当 f() 执行的时候出现 panic 的时候(被外层 recover,进程没挂),会导致没有 o.done 加计数,但其实 f() 已经执行过了,这就违反语义了。
源码注释中还提到了2个有趣的点:
-
为什么需要将done 放在第一个字段?
// 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.热路径(hot path)是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done,在热路径上是比较好理解的,如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。
为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为,访问第一个字段的机器代码更紧凑,速度更快。
- 以及 为什么不用cas 操作检查计数?
// 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.