go并发编程-sync.Once
go并发编程-sync.Once
sync.Once
是 Go 标准库提供的使函数只执行一次的对象,常应用于单例模式,例如初始化配置、保持数据库连接等。它可以在代码的任意位置初始化和调用,可以延迟到使用时再执行,并且在并发场景下是线程安全的。
sync.Once首次使用后不可复制,否则无法起到限制函数只能执行一次的作用。
使用场景
type Config struct{}
var Instance Config
var once sync.Once
func InitConfig() *Config {
once.Do(func(){
Instance = &Config{}
})
return Instance
}
多数情况下,sync.Once用来控制变量的初始化:
- 当且仅当第一次访问某个变量时,进行初始化(写);
- 变量初始化过程中,所有读都被阻塞,直到初始化完成;
- 变量仅初始化一次,初始化完成后驻留在内存里。
与init()的区别
通常我们也会使用init()方法进行初始化,与sync.Once不同的是,init()方法是在其所在的package首次加载时执行的,一方面延长了程序加载时间,另一方面,如果初始化的变量一直没有使用,还会造成内存空间的浪费。
源码解析
type Once struct {
done int32
m Mutex
}
Once结构体中只有两个字段:
- done:表示操作是否已完成;
- m:用来保证线程安全。
关于为什么要把done放在结构体的前面?
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
什么是hot path?
hot path 代表了一系列执行非常频繁的指令。
当访问结构体的第一个字段时,我们可以直接获取结构的指针以访问第一个字段;
如果要访问其他字段,除了需要拿到结构体指针,我们还需要得到第一个字段的偏移量才能访问到第二个字段的地址。对于机器代码来说,CPU必须对struct指针执行额外偏移量的加法运算才能获得要访问的值的地址。
因此,将done放在结构体的前面,可以使得机器指令更加紧凑且计算量更少。
http://47.103.99.11/archives/180/
stackoverflow.com/questions/5…
Once具体实现
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done == 0) {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
上面的代码是sync.Once的官方实现,比较简单,当o.done==0
时,代表f还没有执行,进入到doSlow
方法中,首先获取sync.Once的锁,加锁成功则继续执行,否则进入等待,直到mutex锁释放。f执行成功后设置done的值,标志f执行完成。
我们再来看看下面这段代码:
func (o *Once) Do(f func()) {
if atomic.CompareAndSwapUint32(&o.done, 0, 1){
f()
}
}
Do需要保证当它返回时,f已经执行完成。
上面这种实现则无法保证Do返回的时候f已经执行完成。如果同时有两个线程调用Do方法,此时成功拿到CAS锁的线程将继续执行f,而获取失败的线程则直接返回,而此时f可能还未执行完成。
这就是为什么doSlow会使用到mutex,主要为了保证在并发场景下,f只执行一次,且atomic.LoadUint32必须等到f执行完成后才能返回。
参考文档
转载自:https://juejin.cn/post/7135685511657553957