探索 Go sync.Once 的实现原理Go 语言中 sync.Once 的实现原理、注意事项和常见应用,包括原子操作
前言
在代码开发过程中,我们有时需要一个对象只有一个实例,例如: 某个数据库的客户端实例, 某个全局配置等。这也是我们常说的单例模式,实现单例模式有两种方式:饿汉模式、 懒汉模式。
饿汉模式:通常在服务启动时就完成了单例的初始化工作,在 GO 中通常都是在 main 函数或 init 函数里进行初始化工作, main函数 、 Init 函数天然就支持只初始化一次的要求。
懒汉模式:通常是单例被使用时才被初始化,但是在程序的整个生命周期里会多次使用单例,甚至会并发使用,所以使用懒汉模式时,需要确保单例只被初始化一次,还要保证并发安全。Go 语言中的 sync.Once 就是一个优雅且并发安全的解决方案。
sync.Once 是什么
在 Go 语言中,sync.Once是一个同步原语,用于确保某个操作只被执行一次。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initData() {
fmt.Println("Initializing data...")
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(initData)
}()
}
wg.Wait()
}
$ go run main.go
Initializing data... // 只初始化一次
sync.Once 实现原理
Once 结构体
sync.Once 是一个结构体,里面有 done、m 两个字段:
type Once struct {
done atomic.Uint32
m Mutex
}
-
done:表示该操作是否已经执行,0 代表未执行,1 代表已经执行。
-
m: 互斥锁,确保只有一个协程执行操作。
done 和 m 都不是引用类型,所以 sync.Once 被复制后就没办法保证只执行一次了。
Once.Do
Once.Do 是 sync.Once 的核心逻辑,首先 Do 会判断 done 的状态,如果 done == 0 说明第一次没有执行或者是没执行完,然后会进入 doSlow 流程,否则会直接退出,这里使用 done.Load() 来加载 done 状态的,没有使用锁是为了提升性能。
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
之前《一文搞定 Go 原子操作》也提到过Load() 操作是原子的,但是 o.done.Load() == 0 的 if 判断并不是原子的,这里分两种情况讨论:
-
当 done 不等于 0 时,也就是 == 1 时,这时不再会有协程来改变 done 的状态,所以可以直接返回,速度比较快。
-
当 done 等于 0 时,这时可能会有多个协程去竞争改变 done 的值,所以 doSlow 里需要加锁,来保证只有一个协程去改变 done 的值,速度会较慢一些。
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.Store(1) 要在 f() 执行完后再执行,否则会出现资源还没初始化完成就被使用的情况。
官方文档也给了一个例子, 下面这种实现是错误的, 因为 Do 需要保证当它返回时,f() 已经执行完毕:
func (o *Once) Do(f func()) {
if o.done.CompareAndSwap(0, 1) {
f()
}
}
sync.Once 的注意事项
sync.Once 不能被复制
sync.Once 一旦被复制,就失去只执行一次的限制,sync.Once 的实现原理是判断 Once 里面的done 的状态,Once 只能保证一个Once实例只执行一次。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initData() {
fmt.Println("Initializing data...")
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
once := once // 复制一个 once
defer wg.Done()
once.Do(initData)
}()
}
wg.Wait()
}
$ go run main.go
Initializing data...
Initializing data...
Initializing data...
Initializing data...
Initializing data...
sync.Once 不能嵌套使用
doSlow 里面使用了互斥锁 ,在 doSlow 没有返回前不会释放锁,doSlow如果要返回就需要执行完 f(),如果 f() 中也请求了锁就会造成相互等待, 就会出现死锁。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func main() {
once.Do(func(){
once.Do(func(){
fmt.Println("内层 once")
})
fmt.Println("外层 once")
})
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0x579bc0, 0x4b6500, 0x1)
/usr/local/go-1.13.5/src/runtime/sema.go:71 +0x47
sync.(*Mutex).lockSlow(0x579bbc)
/usr/local/go-1.13.5/src/sync/mutex.go:138 +0xfc
sync.(*Mutex).Lock(...)
/usr/local/go-1.13.5/src/sync/mutex.go:81
sync.(*Once).doSlow(0x579bb8, 0x4c96b8)
/usr/local/go-1.13.5/src/sync/once.go:62 +0x122
sync.(*Once).Do(...)
/usr/local/go-1.13.5/src/sync/once.go:57
main.main.func1()
/box/main.go:13 +0xa9
sync.(*Once).doSlow(0x579bb8, 0x4c96c0)
/usr/local/go-1.13.5/src/sync/once.go:66 +0xe3
sync.(*Once).Do(...)
/usr/local/go-1.13.5/src/sync/once.go:57
main.main()
/box/main.go:12 +0x4d
Exited with error status 2
sync.Once 可能执行失败
通过源码可知,无论 f() 执行的结果如何,sync.Once 都不允许执行第二遍,所以就会出现一种情况:f() 执行失败,然后想重试也重试不了的情况。所以需要再 f() 内部做一些重试、或者错处理的操作。
var client *goredis.Client
var once sync.Once
func getRedisClient() *goredis.Client {
once.Do(func() {
var err error
// 重试逻辑
for i:=0; i++; i<3 {
client,err = goredis.NewClient(xxxx)
if err == nil {
return
}
}
})
return client
}
sync.Once 的常见应用
sync.Once主要用于确保某个操作只执行一次,在很多场景下都非常有用。
单例模式实现
在 Go 语言中,可以使用sync.Once来实现单例模式,确保一个类型的实例在整个程序中只被创建一次。
package main
import (
"fmt"
"sync"
)
type singleton struct {
data int
}
var instance *singleton
var once sync.Once
func getInstance() *singleton {
once.Do(func() {
instance = &singleton{data: 42}
})
return instance
}
func main() {
s1 := getInstance()
s2 := getInstance()
fmt.Println(s1 == s2) // true,说明是同一个实例
}
懒加载资源
当某些资源的初始化比较耗时或者不是在程序启动时就立即需要,可以使用sync.Once进行懒加载。
package main
import (
"fmt"
"sync"
)
var resourceLoaded bool
var resourceData string
var loadResourceOnce sync.Once
func loadResource() {
fmt.Println("Loading resource...")
resourceData = "This is the loaded resource data."
resourceLoaded = true
}
func getResource() string {
if!resourceLoaded {
loadResourceOnce.Do(loadResource)
}
return resourceData
}
func main() {
fmt.Println(getResource())
fmt.Println(getResource())
}
总结
-
实现原理:
- 原子操作保证高性能;
- 互斥锁保证程序只执行一次,且 Do 返回前 f() 已经执行完。
-
注意事项:
- sync.Once 不能被复制
- sync.Once 不能嵌套使用
- sync.Once 可能执行失败
-
常见应用:本质就是只执行一次
- 单例模式
- 懒加载
转载自:https://juejin.cn/post/7411080402272796698