为什么atomic.StoreUint32比sync.Once中的普通分配更受欢迎?

2024-02-14

在阅读Go源码时,我对src/sync/once.go中的代码有一个疑问:

func (o *Once) Do(f func()) {
    // 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.

    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()
    }
}

Why is atomic.StoreUint32使用,而不是说o.done = 1?这些不是等价的吗?有什么区别?

我们必须使用原子操作(atomic.StoreUint32)以确保其他 goroutine 可以观察到效果f() before o.done在内存较弱的机器上设置为 1 吗?


请记住,除非您手动编写程序集,否则您不是针对机器的内存模型进行编程,而是针对 Go 的内存模型进行编程。这意味着即使原始分配对于您的架构来说是原子的,Go 也需要使用原子包来确保所有支持的架构的正确性。

访问done互斥体之外的flag只需要安全,不需要严格排序,因此可以使用原子操作而不是总是用互斥体获取锁。这是一种使快速路径尽可能高效的优化,允许sync.Once用于热路径。

互斥锁用于doSlow仅用于该函数内的互斥,以确保只有一个调用者能够实现f()之前done标志已设置。该标志是使用atomic.StoreUint32,因为它可能同时发生atomic.LoadUint32位于受互斥锁保护的临界区之外。

正在阅读done字段与写入(甚至原子写入)同时进行,是一种数据竞争。仅仅因为该字段是原子读取的,并不意味着您可以使用正常的赋值来写入它,因此首先检查该标志atomic.LoadUint32并写有atomic.StoreUint32

直接读取的done within doSlow is安全,因为它受到互斥锁的并发写入保护。同时读取值atomic.LoadUint32是安全的,因为两者都是读操作。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么atomic.StoreUint32比sync.Once中的普通分配更受欢迎? 的相关文章