通过使用sync.Once实现“完美单例”?

fly*_*101 -3 synchronization go

我很困惑,下面的代码片段完美吗?

import "sync"
import "sync/atomic"

var initialized uint32
var instance *singleton

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}
Run Code Online (Sandbox Code Playgroud)

atomic.StoreUint32(&initialized, 1)会将实例刷新到所有CPU吗?我想我需要添加一个原子存储并加载,例如下面的代码片段

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        atomic.StorePointer(&instance, &singleton{})
    })
    return atomic.LoadPointer(&instance)
}
Run Code Online (Sandbox Code Playgroud)

我认为 Once.Do 只能保证函数 f 执行一次。并且atomic.StoreUint32(&o.done, 1)只是 o.done 的内存障碍。它不能确保instance全局可见

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    // Slow-path.
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}
Run Code Online (Sandbox Code Playgroud)

poy*_*poy 9

让我们把你的问题分成两部分:

  1. 单例
  2. 原子和 Go 内存模型

单例

Go 有包级变量。这些是在任何东西有机会移动之前实例化的,因此,如果您擅长在使用包后立即创建这些东西,您将免费获得一个单例。

package somepack

var(
  connection = createConn()
)

func Connection() SomeConnection {
  return connection
}
Run Code Online (Sandbox Code Playgroud)

connection将被创建一次,因此Connection()将安全地返回它的相同实例。

有时,当开发人员想要“惰性”实例化时,他们会选择单例。如果创建资源的成本昂贵且并不总是需要,那么这是一个好主意。这就是sync.Once有用的地方。

var (
  connection SomeConnection // Not instantiated
  connectionOnce sync.Once
)

func Connection() SomeConnection {
  connectionOnce.Do(func(){
    connection = createConn()
  })

  return connection
}
Run Code Online (Sandbox Code Playgroud)

请注意,我没有对作业做任何特殊的事情(例如,atomic.Store())。这是因为sync.Once它负责确保安全所需的所有锁定。

原子和 Go 内存模型

已发布的文档是一个很好的入门资源:Go 内存模型

您对“刷新”到不同 CPU 的担忧是有效的(尽管有一些注释),因为每个 CPU 都有自己的缓存和自己的状态。C++(以及 Rust 等其他语言)开发人员往往会关心这一点,因为他们会关心这一点。Go 开发人员不会太关心,因为 Go 只“发生过”。事实上 Rust 有一些很好的文档

话虽如此,您通常不需要担心它。互斥锁(和sync.Once)将强制每个 CPU 上的内存状态达到您所期望的状态。