什么是sync.Once
sync.Once 是 Go 语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do,该方法接收一个函数参数。在 Do 方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。
例子
func main() {var once sync.Oncefor i := 0; i < 5; i++ {go func(i int) {fun1 := func() {fmt.Printf("i:=%d\n", i)}once.Do(fun1)}(i)}time.Sleep(1 * time.Second)
}
无论执行多少次,仅返回 i:=0
看看源码
//Once 是一个只执行一次的对象。
//Once 首次使用后不得复制一次。
type Once struct {//done表示动作是否已执行。// 它在结构中是第一个,因为它在hot路径中使用。// 热路径在每个调用站点都是内联的done uint32m Mutex
}
唯一的对外开放函数(Do)
func (o *Once) Do(f func()) {//one为0则表示未执行过,调用doSlow()方法初始化if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}
}
Do(f) 被调用多次,即使f在每次调用中都有不同的值,只有第一次调用才会调用f。
func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done == 0 { // 双重检查,避免 f 已被执行过defer atomic.StoreUint32(&o.done, 1)f()}
}
只有done == 0 才会被释放,并且使用defer保证 f()执行完会讲done置1
我们说说Once一些其他的事吧
不要嵌套 同一个once
func main() {var once sync.Oncefor i := 0; i < 5; i++ {once.Do(func() {once.Do(func() {fmt.Println(1)})})}time.Sleep(1 * time.Second)
}
直接报错:
作者在代码中提示我们:因为在对f的一个调用返回之前,不会返回对Do的调用,所以如果f导致调用Do,它将死锁。
解释 atomic.CompareAndSwapUint32 为什么不行
作者提到了 atomic.CompareAndSwapUint32 是存在问题:给定两个同时调用,cas的获胜者将调用f,第二个将立即返回,而无需等待第一个对f的调用完成。
func (o *Once) Do(f func()) {if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f()}
}
虽然 cas 保证了同一时刻只有一个请求进入 if 判断执行 f()。但是其它的请求却没有等待 f() 执行完成就立即返回了。那么用户端在执行 once.Do 返回之后其实就可能存在 f() 还未完成,就会出现意料之外的错误。如下面例子
type OnceSelf struct {done uint32
}func (o *OnceSelf) Do(f func()) {if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f() }
}func main() {var b map[string]intvar once OnceSelfgo func() {once.Do(func() {b = make(map[string]int, 10)})}()b["1"] = 1fmt.Println(b)}
会存在问题
init 和 once区别
- init 函数是在文件包首次被加载的时候执行,且只执行一次
- sync.Onc 是在代码运行中需要的时候执行,且只执行一次
链接
what-is-fast-path-slow-path-hot-path
what-does-hot-path-mean-in-the-context-of-sync-once