6.3. 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是很常用的,也很适合单例模式使用,源码简单明了,以后在项目中多多使用吧!


关注和赞赏都是对笔者最大的支持
关注和赞赏都是对笔者最大的支持

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~