sync.Once
sync.Once的源码十分简单,而且注释十分清楚,直接来看一下。
源码
定义了一个结构体Once
type Once struct {
done uint32 // 是否执行过,初始值为0
m Mutex // 锁
}
对外提供了一个方法Do,Once.Do可以理解成资源初始化,只会执行一次。
func (o *Once) Do(f func()) {
// 这里保证原子性的读取o.done,如果未执行0,调用doSlow
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // 锁住
defer o.m.Unlock() // 最后释放锁
// 如果未执行过f,就执行f,并修改o.done为1
// 这里已经加锁了,保证了原子性,不需要使用atomic.LoadUint32
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
如果你熟悉atomic,这里你可能会有个疑问,Do方法里面为何不直接使用cas原子操作呢,那多简洁?
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
其实源码注释里已经有了说明,这样的实现并不合理。
当你有2个并发请求调用Do,这样的实现确实能保证只会调用一次f。但是,假如f的执行需要一段时间,比如初始化数据库连接池,当f执行尚未完成,并发中另一个请求因为没有执行原子操作直接返回了,使用f中初始化的连接池就必然会失败,那么这样的实现显然是不可取的。
所以必须确保f执行完成之后,才能将done置为1。
使用
var one sync.Once
fun1 := func() {
fmt.Println("do one")
}
fun2 := func() {
fmt.Println("do two")
}
one.Do(fun1)
one.Do(fun2)
output:
do one
可以看到只执行了一次,也就是fun1。
其实once很适合应用到单例模式,比如连接数据库,
package db
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/spf13/viper"
)
var once sync.Once
var db *gorm.DB
// 单例模式获取*gorm.DB
func GetDB() *gorm.DB {
once.Do(func() {
db = openPool()
})
return db
}
func openPool() *gorm.DB {
...
}
好了,Once是很常用的,也很适合单例模式使用,源码简单明了,以后在项目中多多使用吧!
sync.Once
https://blog.puresai.com/2021/02/06/syncOnce/