Go语言之 sync.Once

sync.Oncesync 包下的一个组件,它保证某些操作只会被执行一次,无论有多少个 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.LoadUint32atomic.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 适用于初始化操作、单例模式等需要确保只执行一次的场景。

打 赏