在阅读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()
    }
}
为什么使用ataomic.StoreUint32而不是说o.done = 1?这些不相等吗?有什么区别?
在内存模型较弱的机器上将o.done设置为1之前,是否必须使用原子操作(atomic.StoreUint32)确保其他goroutine可以观察到“f()”的作用?

最佳答案

请记住,除非您用手工编写程序集,否则您不是在对计算机的内存模型进行编程,而是在对Go的内存模型进行编程。这意味着即使您的体系结构中的原始分配都是原子的,Go仍需要使用原子包来确保所有受支持的体系结构的正确性。
仅需要安全地访问互斥锁之外的done标志,而不必严格地进行访问,因此可以使用原子操作,而不是始终使用互斥锁获得锁定。这是一项优化,以使快速路径尽可能高效,从而允许在热路径中使用sync.Once
用于doSlow的互斥锁仅可在该函数内互斥,以确保在设置f()标志之前只有一个调用者将其设为done。该标志是使用atomic.StoreUint32编写的,因为它可能与mutt保护的关键部分之外的atomic.LoadUint32同时发生。
与写入(甚至原子写入)同时读取done字段是一种数据竞争。仅仅因为该字段是原子读取的,并不意味着您可以使用常规分配来编写该字段,因此首先使用atomic.LoadUint32检查该标志,然后使用atomic.StoreUint32写入该标志
done中直接读取doSlow是安全的,因为互斥锁可以防止它被并发写入。与atomic.LoadUint32并发读取值是安全的,因为两者都是读取操作。

关于go - 为什么在sync.Once中,atomic.StoreUint32比普通分配更可取?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/65935091/

10-12 23:42