Go语言之 sync.Once

sync.Once 是 sync 包下的一个组件,它保证某些操作只会被执行一次,无论有多少个 goroutine 调用它。常见的使用场景包括初始化操作,确保某段代码(如初始化函数)只执行一次。

关键思想:通过原子操作和内存屏障的配合,确保在多线程环境下某些操作只会执行一次。

基本使用

sync.Once 使用起来非常简单,只需要调用 Do 方法,传入一个无参无返回值的函数即可。

以下为基本使用示例:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once

func initialize() {
    fmt.Println("初始化操作")
}

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(initialize)
        }()
    }
}

运行结果:

初始化操作

即使有多个 goroutine 调用 once.Do(initialize),initialize 函数也只会被执行一次。

2. 源码分析

Once 结构体

sync.Once 的结构体定义如下:

type Once struct {
    m    Mutex
    done uint32
}
  • m:互斥锁,确保同一时间只有一个 goroutine 执行 Do 方法。
  • done:标志变量,记录操作是否已经执行。

大致流程

Do 方法

Do 方法是 sync.Once 的核心,其实现如下:

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

主要逻辑包括:

  1. 首先通过原子操作 LoadUint32 读取 done 变量,如果为 1,表示操作已经执行,直接返回。
  2. 否则,加锁,防止其他 goroutine 并发执行。
  3. 再次检查 done 变量,确保在加锁前没有其他 goroutine 执行。
  4. 执行传入的函数 f,并在执行完成后将 done 变量设置为 1。

3. 详细源码分析

原子操作

sync.Once 使用了 atomic.LoadUint32 和 atomic.StoreUint32 来读取和设置 done 变量。原子操作可以确保在多线程环境下的安全性和高效性。

import "sync/atomic"

atomic.LoadUint32(&o.done)
atomic.StoreUint32(&o.done, 1)

内存屏障

内存屏障用于防止编译器和 CPU 进行指令重排,确保 done 变量的读写操作的顺序性。在 sync.Once 中,原子操作隐含了内存屏障的功能。

锁机制

sync.Once 使用了互斥锁(Mutex)来确保某些操作在多 goroutine 环境下的安全性。加锁可以确保临界区的代码段在同一时间只能被一个 goroutine 执行。

o.m.Lock()
defer o.m.Unlock()

4. 使用案例

案例一:数据库连接初始化

在高并发的应用中,可以使用 sync.Once 确保数据库连接只被初始化一次。

package main

import (
    "database/sql"
    "fmt"
    "sync"
)

var once sync.Once
var db *sql.DB

func initializeDB() {
    var err error
    db, err = sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        panic(err)
    }
}

func getDB() *sql.DB {
    once.Do(initializeDB)
    return db
}

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            db := getDB()
            fmt.Println(db)
        }()
    }
}

案例二:单例模式

在实现单例模式时,可以使用 sync.Once 确保对象只被初始化一次。

package main

import (
    "fmt"
    "sync"
)

type Singleton struct{}

var (
    instance *Singleton
    once     sync.Once
)

func getInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            s := getInstance()
            fmt.Println(s)
        }()
    }
}

5. 使用场景及总结

  1. 关键思想是通过原子操作和内存屏障,确保某些操作在多线程环境下只执行一次。
  2. sync.Once 使用起来非常简单,只需要调用 Do 方法,传入一个无参无返回值的函数即可。
  3. 注意:Do 方法传入的函数不能有返回值,因为 sync.Once 不会处理函数的返回值。
  4. sync.Once 适用于初始化操作、单例模式等需要确保只执行一次的场景。

打 赏