文章目录

Go语言sync.Once为什么能保证只执行一次

发布于 2026-05-11 04:40:26 · 浏览 14 次 · 评论 0 条

Go语言sync.Once为什么能保证只执行一次

sync.Once 是 Go 标准库中一个非常实用的工具,用于确保某个操作在程序运行期间只执行一次。无论有多少个 goroutine 调用 Do 方法,传入的函数都只会被执行一次。这种机制在单例模式、资源初始化等场景中非常有用。本文将深入剖析 sync.Once 的内部实现,解释它如何保证“只执行一次”。

1. sync.Once 的结构

要理解 sync.Once 的工作原理,首先需要了解它的结构体定义。

type Once struct {
    done uint32
    m    Mutex
}
  • done:一个无符号 32 位整数,作为标记位。如果 done 的值为 0,表示函数尚未执行;如果值为 1,表示函数已经执行过。
  • m:一个互斥锁(Mutex),用于保证在检查和设置 done 时的线程安全。

2. Do 方法的执行流程

sync.Once 的核心方法是 Do,它接受一个无参数、无返回值的函数作为参数。让我们通过源码来分析其执行流程。

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        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()
    }
}

2.1 第一次检查:快速路径

  1. 检查 done 的值。atomic.LoadUint32(&o.done) 使用原子操作读取 done 的值。
  2. 如果 done 的值是 1,说明函数已经执行过,Do 方法直接返回,不会执行任何其他操作。这是“快速路径”,避免了不必要的加锁和解锁,提高了性能。

2.2 第二次检查:慢速路径

  1. 如果 done 的值是 0,说明函数尚未执行,需要进入“慢速路径”。
  2. 加锁 m。这确保了在接下来的操作中,只有一个 goroutine 能进入临界区。
  3. 在锁内,再次检查 done 的值。这是至关重要的一步,称为“双重检查”。
  4. 如果 done 仍然是 0,说明当前 goroutine 是第一个到达这里的,需要执行函数。
  5. 执行传入的函数 f()
  6. 设置 done1atomic.StoreUint32(&o.done, 1) 使用原子操作将 done 的值设置为 1,标记函数已执行。
  7. 解锁 m

3. 关键机制解析

3.1 双重检查锁定(Double-Checked Locking)

sync.Once 使用了双重检查锁定模式,这是保证高效和线程安全的关键。

  • 第一次检查:在加锁之前检查 done。如果 done 已经是 1,直接返回,避免了加锁的开销。这对于大多数情况下函数已经执行过的情况非常高效。
  • 第二次检查:在加锁之后再次检查 done。这是为了防止多个 goroutine 同时通过第一次检查(因为第一次检查不是原子的),导致它们都尝试加锁。只有第一个成功加锁的 goroutine 会执行函数,后续的 goroutine 在加锁后再次检查 done 时会发现它已经被设置为 1,从而不会重复执行函数。

3.2 原子操作

done 字段的读写操作使用原子操作(atomic.LoadUint32atomic.StoreUint32),这保证了:

  • 原子性:读取或设置 done 的操作是原子的,不会被其他 goroutine 中断。
  • 可见性:当一个 goroutine 设置 done1 后,其他 goroutine 立即能看到这个变化。

3.3 互斥锁的作用

m 互斥锁确保了在执行函数 f() 和设置 done 的过程中,只有一个 goroutine 能进入临界区。这防止了多个 goroutine 同时执行函数,从而保证了“只执行一次”的语义。

4. 代码示例

下面是一个使用 sync.Once 的完整示例,展示了其用法和效果。

package main

import (
    "fmt"
    "sync"
    "time"
)

var once sync.Once

func main() {
    for i := 0; i < 10; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d is trying to call Do\n", id)
            once.Do(func() {
                fmt.Printf("Goroutine %d is executing the function\n", id)
                time.Sleep(1 * time.Second) // 模拟耗时操作
            })
            fmt.Printf("Goroutine %d finished\n", id)
        }(i)
    }

    time.Sleep(3 * time.Second) // 等待所有 goroutine 完成
}

运行上述代码,你会看到:

  1. 所有 goroutine 都会尝试调用 Do 方法。
  2. 只有一个 goroutine 会执行传入的函数(打印 "Goroutine X is executing the function")。
  3. 其他 goroutine 在第一次检查 done 时发现它已经是 1,直接返回,不会执行函数。

输出可能类似于:

Goroutine 0 is trying to call Do
Goroutine 1 is trying to call Do
Goroutine 2 is trying to call Do
Goroutine 3 is trying to call Do
Goroutine 4 is trying to call Do
Goroutine 5 is trying to call Do
Goroutine 6 is trying to call Do
Goroutine 7 is trying to call Do
Goroutine 8 is trying to call Do
Goroutine 9 is trying to call Do
Goroutine 0 is executing the function
Goroutine 0 finished
Goroutine 1 finished
Goroutine 2 finished
Goroutine 3 finished
Goroutine 4 finished
Goroutine 5 finished
Goroutine 6 finished
Goroutine 7 finished
Goroutine 8 finished
Goroutine 9 finished

注意:第一个执行函数的 goroutine ID 可能是任意一个,因为它们几乎同时到达 Do 方法。

5. 总结

sync.Once 通过结合 标记位(done原子操作互斥锁(m,实现了高效且线程安全的“只执行一次”机制。

  • done 标记位:用于记录函数是否已执行。
  • 原子操作:保证 done 读写操作的原子性和可见性。
  • 互斥锁:确保在执行函数和设置 done 时的线程安全。
  • 双重检查:避免不必要的加锁,提高性能,同时防止竞态条件。

这种设计使得 sync.Once 在保证功能正确性的同时,也具有很好的性能表现。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文