从 Golang 的 Once 实现闲聊
Once
(opens in a new tab) 的实现包含了大量篇幅的注释,和经过了非常多数量的 commit,从经验上看大概率有非常多值得学习之处。而且实现也不长,大概几十行。
使用 Once 的好处和最小例子
Once 的作用可以概括为 保证 once.Do(f)
可以:
-
被调用任意次数
-
once 实例被复制到任意数量的 goroutine 中并发调用
f
都只会被执行一次,并且所有调用方都会阻塞直到 f
执行结束,一个使用 once
的例子:
import (
"fmt"
"sync"
)
func main() {
var (
once sync.Once
w sync.WaitGroup
age int
)
g := func() {
age++
}
for i := 0; i < 5; i++ {
w.Add(1)
go func() {
once.Do(g)
w.Done()
}()
}
w.Wait() // 等待所有 goroutine 执行结束
fmt.Println(age) // 输出 1,而不是 5, g 只会被执行一次
}
注释中解释了哪些
- Once 持有
atomic.Uint32
来保证 f 只执行一次。但同时还需要持有Mutex
来保证f
的执行结束发生在所有once.Do(f)
的调用返回之前。配合方式可以概括为,所有的once.Do(f)
都会在atomic.Uint32
的值为 0 时,去尝试获取Mutex
(拿到锁之后,重新判断atomic.Uint32
的值,如果已经不是 0,就提前返回。因此只有一次调用会实际执行 f,其他的调用都只是用 Mutex 来同步 f 的执行完成时机)来执行f
,执行结束就会改变atomic.Uint32
的值为 1,然后再解锁(通过 defer 的 LIFO 栈来保证先后顺序)。
// Note: Here is an incorrect implementation of Do:
//
// if o.done.CompareAndSwap(0, 1) {
// f()
// }
// ... https://github.com/golang/go/blob/master/src/sync/once.go#L55C1-L61C60
once.Do(f)
没有参数,但可以通过闭包的方式传参
func onceCallWithParam() {
var once sync.Once
age, step := 0, 5
once.Do(func() {
increaseBy(&age, step)
})
}
func increaseBy(age *int, step int) {
*age += step
}
- 使用 Once 封装成的 OnceFunc,如果 f 首次执行会 panic,后续都会 panic。并且第一次是什么 panic 结果,后面都会是相同的 panic。是通过持有
var valid bool
(保存是否 panic 过)和var p any
(保存 recover 得到的值) 来实现的。
一些碎碎念
最上面的例子为了更清楚所以定义了单独的 g,其实放到匿名函数中也是被认可的做法,例如 once_test 的单测 (opens in a new tab) 有类似的用法。
once.Do(func() {
age++
})