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/
作者
puresai
许可协议