sync.Once

作者: wayyyy | 来源:发表于2022-05-03 00:19 被阅读0次

利用 sync.once 实现单例

var s *SomeObject   // 全局变量(我们只希望创建一个)
var once sync.Once  // 定义一个 once 变量

func GetInstance() *SomeObject {
    once.Do(func() {
        s = &SomeObject{}  // 创建一个对象,赋值指针给全局变量
    })
    return s
}
sync.Once 实现
type Once struct {
    done uint32  // 保证变量仅被初始化一次,需要有个标志来判断变量是否已初始化过,若没有则需要初始化
    m    Mutex  // 线程安全,支持并发,无疑需要互斥锁来实现
}

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 { 
        o.doSlow(f)  // 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。
    }
}

func (o *Once) doSlow(f func()) {
    // 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
      // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。
      defer atomic.StoreUint32(&o.done, 1)
      f()
    }
}

为什么要用defer 来加计数?不直接在后面执行计数

if o.done == 0 {
    defer atomic.StoreUint32(&o.done, 1)
    f()
}

因为处理不了 panic 的异常,举个例子:
如果不用 defer ,当 f() 执行的时候出现 panic 的时候(被外层 recover,进程没挂),会导致没有 o.done 加计数,但其实 f() 已经执行过了,这就违反语义了。

源码注释中还提到了2个有趣的点:

  1. 为什么需要将done 放在第一个字段?

    // done indicates whether the action has been performed.
    // It is first in the struct because it is used in the hot path.
    // The hot path is inlined at every call site.
    // Placing done first allows more compact instructions on some architectures (amd64/386),
    // and fewer instructions (to calculate offset) on other architectures.

    热路径(hot path)是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done,在热路径上是比较好理解的,如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。

    为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为,访问第一个字段的机器代码更紧凑,速度更快。

  1. 以及 为什么不用cas 操作检查计数?

    // Note: Here is an incorrect implementation of Do:
    //
    // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    // f()
    // }
    //
    // Do guarantees that when it returns, f has finished.
    // This implementation would not implement that guarantee:
    // given two simultaneous calls, the winner of the cas would
    // call f, and the second would return immediately, without
    // waiting for the first's call to f to complete.
    // This is why the slow path falls back to a mutex, and why
    // the atomic.StoreUint32 must be delayed until after f returns.

相关文章

  • 深度剖析Golang sync.Once源码

    目录 什么是sync.Once 如何使用sync.Once 源码分析 什么是sync.Once Once 可以用来...

  • sync.Once

    利用 sync.once 实现单例 sync.Once 实现 为什么要用defer 来加计数?不直接在后面执行计数...

  • Go语言——sync.Once分析

    Go语言——sync.Once分析 sync.Once表示只执行一次函数。要做到这点,就需要两点: 计数器,统计函...

  • golang sync.Once源代码阅读--让你的初始化只执行

    sync.Once的用处 sync.Once 主要用途执行只需要执行一次的初始化函数,比如验证器的初始,http....

  • sync.Once

    sync.Once 的使用场景 sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式...

  • go 中的 sync.Once

    sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,sync.Once 仅提供了一...

  • sync.Once

    once结构是这样的 其中只有这么一个方法 作用是保证多个协程只执行某个函数一次为什么不能使用CAS原子操作来替代...

  • golang 系列:sync.Once 讲解

    sync.Once 介绍 之前提到过 Go 的并发辅助对象:WaitGroup[https://mp.weixin...

  • sync.Once分析

    使用场合 在实际生产中,很多时候我们需要执行只有一次的操作(如单例模式)。在golang中,我们可以简单的使用sy...

  • Go - sync.Once

    设计目的 Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资...

网友评论

      本文标题:sync.Once

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