sync.Once 基本概念
什么是 sync.Once
sync.Once 是 Go 语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do,该方法接收一个函数参数。在 Do 方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。
sync.Once 的应用场景
sync.Once 主要用于以下场景:
单例模式:确保全局只有一个实例对象,避免重复创建资源。
延迟初始化:在程序运行过程中需要用到某个资源时,通过 sync.Once 动态地初始化该资源。
只执行一次的操作:例如只需要执行一次的配置加载、数据清理等操作。
sync.Once 应用实例
单例模式
在单例模式中,我们需要确保一个结构体只被初始化一次。使用 sync.Once 可以轻松实现这一目标。
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetInstance()
fmt.Printf("Singleton instance address: %p\n", s)
}()
}
wg.Wait()
}
上述代码中,GetInstance 函数通过 once.Do() 确保 instance 只会被初始化一次。在并发环境下,多个协程同时调用 GetInstance 时,只有一个协程会执行 instance = &Singleton{},所有协程得到的实例 s 都是同一个。
延迟初始化
有时候希望在需要时才初始化某些资源。使用 sync.Once 可以实现这一目标。
package main
import (
"fmt"
"sync"
)
type Config struct {
config map[string]string
}
var (
config *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("init config...")
config = &Config{
config: map[string]string{
"c1": "v1",
"c2": "v2",
},
}
})
return config
}
func main() {
// 第一次需要获取配置信息,初始化 config
cfg := GetConfig()
fmt.Println("c1: ", cfg.config["c1"])
// 第二次需要,此时 config 已经被初始化过,无需再次初始化
cfg2 := GetConfig()
fmt.Println("c2: ", cfg2.config["c2"])
}
在这个示例中,定义了一个 Config 结构体,它包含一些设置信息。使用 sync.Once 来实现 GetConfig 函数,该函数在第一次调用时初始化 Config。这样,我们可以在真正需要时才初始化 Config,从而避免不必要的开销。
sync.Once 实现原理
type Once struct {
// 表示是否执行了操作
done uint32
// 互斥锁,确保多个协程访问时,只能一个协程执行操作
m Mutex
}
func (o *Once) Do(f func()) {
// 判断 done 的值,如果是 0,说明 f 还没有被执行过
if atomic.LoadUint32(&o.done) == 0 {
// 构建慢路径(slow-path),以允许对 Do 方法的快路径(fast-path)进行内联
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
defer o.m.Unlock()
// 双重检查,避免 f 已被执行过
if o.done == 0 {
// 修改 done 的值
defer atomic.StoreUint32(&o.done, 1)
// 执行函数
f()
}
sync.Once 结构体包含两个字段:done 和 mu。done 是一个 uint32 类型的变量,用于表示操作是否已经执行过;m 是一个互斥锁,用于确保在多个协程访问时,只有一个协程能执行操作。
sync.Once 结构体包含两个方法:Do 和 doSlow。Do 方法是其核心方法,它接收一个函数参数 f。首先它会通过原子操作atomic.LoadUint32(保证并发安全) 检查 done 的值,如果为 0,表示 f 函数没有被执行过,然后执行 doSlow 方法。
在 doSlow 方法里,首先对互斥锁 m 进行加锁,确保在多个协程访问时,只有一个协程能执行 f 函数。接着再次检查 done 变量的值,如果 done 的值仍为 0,说明 f 函数没有被执行过,此时执行 f 函数,最后通过原子操作 atomic.StoreUint32 将 done 变量的值设置为 1。
为什么会封装一个 doSlow 方法
doSlow 方法的存在主要是为了性能优化。将慢路径(slow-path)代码从 Do 方法中分离出来,使得 Do 方法的快路径(fast-path)能够被内联(inlined),从而提高性能。
为什么会有双重检查(double check)的写法
从源码可知,存在两次对 done 的值的判断。
第一次检查:在获取锁之前,先使用原子加载操作 atomic.LoadUint32 检查 done 变量的值,如果 done 的值为 1,表示操作已执行,此时直接返回,不再执行 doSlow 方法。这一检查可以避免不必要的锁竞争。
第二次检查:获取锁之后,再次检查 done 变量的值,这一检查是为了确保在当前协程获取锁期间,其他协程没有执行过 f 函数。如果 done 的值仍为 0,表示 f 函数没有被执行过。
通过双重检查,可以在大多数情况下避免锁竞争,提高性能。