美文网首页golang学习篇章
sync.Once 短小精悍

sync.Once 短小精悍

作者: Best博客 | 来源:发表于2020-09-01 11:55 被阅读0次

    sync.Once 源码

    package sync
    
    import (
        "sync/atomic"
    )
    type Once struct {
        done uint32
        m    Mutex
    }
    
    func (o *Once) Do(f func()) {
        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()
        if o.done == 0 {
            defer atomic.StoreUint32(&o.done, 1)
            f()
        }
    }
    

    剔除源码注释之后,才这么几行代码,却能发挥巨大的作用,但里面有些小细节还是值得好好推敲的。

    适合场景:

    1. 全局变量初始化
    2. 懒汉模式的单例
    3. 服务接受系统级别的kill信号去触发业务代码,比如 kill -15 pid。
    4. 所有只允许执行一次的场景。。

    不太适合场景:

    1. 一上来就高并发场景:一上来就高并发场景看源码就会发现逻辑走到 sync.Mutex 里面去控制了,那么这个时候,你倒是不如直接 sync.Mutex 去控制。 不过也就多了个 atomic.LoadUint32 级别的操作呀,所以不足为虑。

    细节刨析

    以下纯搬运,其实源码注释里面也有

    image.png
    答:如果不使用 atomic,无法及时观察doSlow对o.done 的修改。 什么叫及时观察? 首先 要明白sync.Once{}.Do() 它是可能被高频的在多核里面并发并行运行的,内存同步可能是不及时的
    。假如将 atomic 的操作改为普通的 &o.done==0 与 o.done = 1 语句其实也是会起到内存同步的(go里面内存模型一致性的保障是可以通过同步事件去达到的,这里o.m.lock就是同步事件之一),但这个同步事件是发生在f() 函数执行完毕后去同步的(而且还真得等到f执行完毕,后面说..)假如f执行花了5ms,就意味着5ms放进来的并发请求串行全靠 o.m.lock 去保证了。加不加 atomic 都会有lock给你兜底,只是atomic加了 done一旦变化,里面上层的 if atomic.LoadUint32(&o.done) == 0 { 就判断出来了,而不加还需要unlock触发同步事件,很明显前者更及时。

    为啥非要等f() 执行完毕 再通过 o.m.Unlock() 去触发同步事件,达到内存一致性,为啥不把f() 放goroutinue 直接执行 从而更快的触发 同步事件,这样上文的5ms不就不存在了么。

    答:其实这样首先你也只能是缩短5ms这个时间,其次还真不能不等f() 没执行完就触发修改done的值并里面触发同步事件,你想撒,f() 明明还没执行完,你就通知其他人 这个函数已经执行过了,这是不对的,万一后面的业务对 f() 执行结果有强依赖。。。 伪高效埋巨坑。 其实这也是为什么没有用 if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 去替换 atomic.LoadUint32(&o.done) == 0 { 的原因了

    相关文章

      网友评论

        本文标题:sync.Once 短小精悍

        本文链接:https://www.haomeiwen.com/subject/fottdktx.html