本文正在参加「金石方案」
本文首发于稀土,转载请联络授权
作者:陈明勇
大众号:Go技术干货
简介
在某些场景下,咱们需求初始化一些资源,例如单例对象、装备等。完成资源的初始化有多种办法,如界说 package
级别的变量、在 init
函数中进行初始化,或许在 main
函数中进行初始化。这三种办法都能保证并发安全,并在程序启动时完结资源的初始化。
然而,有时咱们期望采用推迟初始化的办法,在咱们真实需求资源的时候才进行初始化,这种需求保证并发安全,在这种情况下,Go
语言中的 sync.Once
供给一个优雅且并发安全的解决方案,本文将对其进行介绍。
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"])
// 第2次需求,此刻 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
办法。这一查看能够避免不必要的锁竞赛。 -
第2次查看:获取锁之后,再次查看
done
变量的值,这一查看是为了保证在当时协程获取锁期间,其他协程没有履行过f
函数。假如done
的值仍为 0,表明f
函数没有被履行过。
经过两层查看,能够在大多数情况下避免锁竞赛,提高功能。
加强的 sync.Once
sync.Once
供给的 Do
办法并没有回来值,意味着假如咱们传入的函数假如发生 error
导致初始化失利,后续调用 Do
办法也不会再初始化。为了避免这个问题,咱们能够完成一个 相似 sync.Once
的并发原语。
package main
import (
"sync"
"sync/atomic"
)
type Once struct {
done uint32
m sync.Mutex
}
func (o *Once) Do(f func() error) error {
if atomic.LoadUint32(&o.done) == 0 {
return o.doSlow(f)
}
return nil
}
func (o *Once) doSlow(f func() error) error {
o.m.Lock()
defer o.m.Unlock()
var err error
if o.done == 0 {
err = f()
// 只要没有 error 的时候,才修正 done 的值
if err == nil {
atomic.StoreUint32(&o.done, 1)
}
}
return err
}
上述代码完成了一个加强的 Once
结构体。与标准的 sync.Once
不同,这个完成允许 Do
办法的函数参数回来一个 error
。假如履行函数没有回来 error
,则修正 done
的值以表明函数已履行。这样,在后续的调用中,只要在没有发生 error
的情况下,才会越过函数履行,避免初始化失利。
sync.Once 的注意事项
死锁
经过剖析 sync.Once
的源码,能够看到它包括一个名为 m
的互斥锁字段。当咱们在 Do
办法内部重复调用 Do
办法时,将会多次测验获取相同的锁。可是 mutex
互斥锁并不支撑可重入操作,因此这将导致死锁现象。
func main() {
once := sync.Once{}
once.Do(func() {
once.Do(func() {
fmt.Println("init...")
})
})
}
初始化失利
这儿的初始化失利指的是在调用 Do
办法之后,履行 f
函数的过程中发生 error
,导致履行失利,现有的 sync.Once
设计咱们是无法感知到初始化的失利的,为了解决这个问题,咱们能够完成一个相似 sync.Once
的加强 once
,前面的内容已经供给了具体完成。
小结
本文具体介绍了 Go
语言中的 sync.Once
,包括它的根本界说、运用场景和运用实例以及源码剖析等。在实践开发中,sync.Once
经常被用于完成单例形式和推迟初始化操作。
尽管 sync.Once
简略而又高效,可是错误的运用可能会造成一些意外情况,需求分外小心。
总之,sync.Once
是 Go
中十分实用的一个并发原语,能够帮助开发者完成各种并发场景下的安全操作。假如遇到只需求初始化一次的场景,sync.Once
是一个十分好的选择。