01
介绍
在 Go 语言中,sync 包有一个 Once 类型,官方文档介绍 Once 是一个只执行一次操作的对象。所以,Once 一般用于并发执行,但只需初始化一次的共享资源。
02
基本用法
Once 的使用也非常简单,Once 只有一个 Do 方法,接收一个无参数无返回值的函数类型的参数 f,不管调用多少次 Do 方法,参数 f 只在第一次调用 Do 方法时执行。
代码示例:
通过阅读示例代码,可以发现代码中定义了两个函数类型的变量 func1 和 func2,分别作为参数传递给两次调用的 Do 方法,执行代码,结果只打印第一次调用 Do 方法传入的 func1 参数的值。
03
实现原理
sync.Once 源码:
type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { // 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。 o.doSlow(f) } } 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() } }
通过阅读 sync.Once 的源码,可以发现 Once 结构体中包含两个字段,分别是 uint32 类型的 done 和 Mutex 类型的 m。并且 Once 实现了两个方法,分别是 Do 和 doSlow。其中 doSlow 是一个非可导出方法,只能被 Do 方法调用。
Done 方法先原子获取 done 的值,如果 done 的值为 0,则调用 doSlow 方法进行二次检查,二次检查时,持有互斥锁,保证只有一个 goroutine 执行操作,二次检查结果仍为 0,则认为是第一次执行,程序执行函数类型的参数 f,然后将 done 的值设置为 1。
04
踩坑
我们已经介绍过 Once 是一个只执行一次操作的对象,假如我们在 Do 方法中再次调用 Do 方法会怎么样呢?代码如下:
阅读代码,我们定义了两个函数类型的变量 func1 和 func2,其中 func1 的函数体内,调用 Do 方法并将 func2 作为参数传递给它,最后调用给定 func1 作为参数的 Do 方法,运行结果是导致程序死锁。所以,记住不要在Do 方法的给定参数中,调用 Do 方法,否则会产生死锁。
05
总结
本文开篇介绍了 Once 的官方定义和使用场景,然后结合示例代码,介绍了 Once 的基本使用,并通过阅读源码,介绍了 Once 的实现原理,最后列举了一个容易踩的「坑」。