Golang Sync.Once 使用与原理,看这一篇足矣
1.1 简介
我们都知道 Go 中 init()
函数可以用于初始化,但是它会在包首次被加载时执行,如果未实际使用,不仅浪费内存,还会延缓程序启动时间。
而 sync.Once
可以在任何位置调用,它允许在实际依赖某个变量时再进行初始化,能实现延迟初始化,并且有效减少性能浪费。
更重要的是,sync.Once
是 Go 语言中的一个同步原语,并发安全。无论有多少个 goroutines 尝试同时执行某些代码,它都可以确保这些代码在并发环境下只执行一次。这在多线程环境中尤为重要,可防止初始化代码被执行多次。
适用场景:
- 初始化全局资源
- 数据库连接池的创建
- 配置文件的加载
- 等等
1.2 实战用法
1.2.1 单例模式
以下是使用 sync.Once
实现单例模式的示例:
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var singleton *Singleton
var once sync.Once
func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create Singleton")
singleton = new(Singleton)
})
return singleton
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
obj := GetSingletonObj()
fmt.Printf("%p\n", obj)
}()
}
wg.Wait()
}
运行上述代码时,输出如下:
Create Singleton
0x574380
0x574380
0x574380
0x574380
0x574380
可以看到,无论有多少 goroutines,Create Singleton
只会执行一次。
1.2.2 关闭 channel
在 Go 语言中,尝试关闭已关闭的 channel 会导致 panic。为了确保 channel 只被关闭一次,可以使用 sync.Once
:
type SafeChannel struct {
ch chan int
once sync.Once
}
// 安全关闭通道
func (sc *SafeChannel) Close() {
sc.once.Do(func() {
close(sc.ch)
})
}
1.2.3 Std case
以下是 html.UnescapeString
函数的线程安全实现,它依赖 sync.Once
确保 entity
和 entity2
字典只初始化一次:
func UnescapeString(s string) string {
populateMapsOnce.Do(populateMaps)
i := strings.IndexByte(s, '&')
if i < 0 {
return s
}
// 其他逻辑...
}
var populateMapsOnce sync.Once
func populateMaps() {
entity = map[string]rune{
"AElig;": '\U000000C6',
"AMP;": '\U00000026',
"Aacute;": '\U000000C1',
// 其他实体定义...
}
}
populateMapsOnce
确保字典只被初始化一次,从而避免在多线程情况下出现问题,并节省内存。
1.3 实现原理
1.3.1 内部结构
在 Go 源码中,sync.Once
的定义如下:
type Once struct {
done atomic.Uint32
m sync.Mutex
}
1.3.2 基本实现
sync.Once
的核心方法是 Do
,其实现如下:
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}
- 快速路径:检查
done
的值是否为 1,如果是,说明已经执行过函数,直接返回。 - 慢速路径:通过加锁确保只有一个 goroutine 执行
f()
,并在执行后将done
设置为 1。
1.3.3 实现特点
- 原子操作:
atomic.Load
和atomic.Store
用于检查和设置done
标志,确保操作的原子性,避免竞态条件。 - 双重检查:在获取锁之前先检查
done
值,避免不必要的锁竞争;获取锁后再检查,确保在并发环境下函数只执行一次。
1.4 自定义优化
可以自定义优化 sync.Once
,例如:只有在没有错误的情况下才跳过函数执行。
package main
import (
"errors"
"fmt"
"sync"
"sync/atomic"
)
type Once struct {
done atomic.Uint32
m sync.Mutex
}
func (o *Once) Do(f func() error) error {
if o.done.Load() == 0 {
return o.doSlow(f)
}
return nil
}
func (o *Once) doSlow(f func() error) error {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
err := f()
if err == nil {
o.done.Store(1)
}
return err
}
return nil
}
func main() {
once := Once{}
var wg sync.WaitGroup
onceFunc := func() error {
fmt.Println("Initializing...")
return errors.New("Error during initialization")
}
var globalErr error
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := once.Do(onceFunc); err != nil {
globalErr = err
}
}()
}
wg.Wait()
fmt.Printf("Global error: %v", globalErr)
}
输出结果:
Initializing...
Initializing...
Initializing...
Initializing...
Initializing...
Global error: Error during initialization
1.5 注意事项
- 合理评估需求,在非并发情况下使用
sync.Once
可能会浪费性能。 - 不要在
Do
函数的f
中嵌套调用Do
,这会导致死锁。 - 避免拷贝
sync.Once
,拷贝后可能导致多次执行函数。
1.6 总结
本文详细介绍了 Go 语言中的 sync.Once
,包括其定义、使用场景、应用实例以及源码分析等。在实际开发中,sync.Once
常被用于单例模式和延迟初始化操作。本文还讲解了 sync.Once
源代码中的以下内容:
- 原子操作的作用
- 双重检查
- 热路径(hot path)
- CAS(Compare-And-Swap)
- 不可拷贝性
我们还提供了一个简单优化版的 sync.Once
,并给出了一些注意事项。
通过这些内容,你可以更好地理解和使用 sync.Once
来提高 Go 语言中的并发安全性和性能。