go源码阅读-sync.Once
go语言基础库sync
中提供了基本的同步原语,包括Once
和waitGroup
等。注意,sync
包中定义的类型均不可被复制。
sync.Once
sync.Once
包中仅对外暴露了一个方法,即Do
方法,该方法仅接受一个f func(){}
作为参数,保障当且仅当第一次调用once市里的Do
方法时,才会执行f func()
。
如果多次调用 once.Do(f)
, 仅有第一次调用时才会调用f
。
常用于单例模式,例如初始化配置、保持数据库连接等
使用方式
如下样例中,使用sync.Once
读取配置文件防止多次调用。
1 | package parallel |
Once和init的区别
- init在所在的 package 首次被加载时执行
- 而Once可以放在任意位置进行初始化和调用,并发场景下安全
sync.Once的实现原理
Once
的结构体如下,使用无符号数标识方法是否已经被执行,同时内部维护互斥锁以保障并发安全。
1 | type Once struct { |
Once
对外暴露如下方法:
1 | func (o *Once) Do(f func()) { |
接收一个func()
作为入参,函数中进行判断once.done
是否已被执行过(此时已经获取互斥锁,所以是直接比较),如果未执行过(未执行记为0),则执行o.doSlow()
方法。
该方法保障当此方法返回时, f
已经被执行了。
1 | func (o *Once) doSlow(f func()) { |
在调用方法时,预先加锁,然后再次进行原子判断是否为被执行过,如果是,则执行传入的方法。
tips
- 为什么要将
done
放置在第一个参数位置
由于Once.done
作为热路径存在,将它放置在结构体的第一个参数位置能够减少CPU指令。因为结构体指针和结构体的第一个参数的地址是一致的。如果将done
放置在其余位置,在寻址时将需要额外的CPU指令完成寻址。 - 上述实现能否修改为如下实现?答案是否定的,因为
1
2
3if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}Do
必须保障return返回时就已经执行f
了,而上述语句在f
未被调用时,如果有两个协程并发调用,其中一个会执行f
;而另外一个会直接返回。不满足上述约束。
参考
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 mqray's blog!
评论