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 第一次检查:快速路径
- 检查
done的值。atomic.LoadUint32(&o.done)使用原子操作读取done的值。 - 如果
done的值是1,说明函数已经执行过,Do方法直接返回,不会执行任何其他操作。这是“快速路径”,避免了不必要的加锁和解锁,提高了性能。
2.2 第二次检查:慢速路径
- 如果
done的值是0,说明函数尚未执行,需要进入“慢速路径”。 - 加锁
m。这确保了在接下来的操作中,只有一个 goroutine 能进入临界区。 - 在锁内,再次检查
done的值。这是至关重要的一步,称为“双重检查”。 - 如果
done仍然是0,说明当前 goroutine 是第一个到达这里的,需要执行函数。 - 执行传入的函数
f()。 - 设置
done为1。atomic.StoreUint32(&o.done, 1)使用原子操作将done的值设置为1,标记函数已执行。 - 解锁
m。
3. 关键机制解析
3.1 双重检查锁定(Double-Checked Locking)
sync.Once 使用了双重检查锁定模式,这是保证高效和线程安全的关键。
- 第一次检查:在加锁之前检查
done。如果done已经是1,直接返回,避免了加锁的开销。这对于大多数情况下函数已经执行过的情况非常高效。 - 第二次检查:在加锁之后再次检查
done。这是为了防止多个 goroutine 同时通过第一次检查(因为第一次检查不是原子的),导致它们都尝试加锁。只有第一个成功加锁的 goroutine 会执行函数,后续的 goroutine 在加锁后再次检查done时会发现它已经被设置为1,从而不会重复执行函数。
3.2 原子操作
done 字段的读写操作使用原子操作(atomic.LoadUint32 和 atomic.StoreUint32),这保证了:
- 原子性:读取或设置
done的操作是原子的,不会被其他 goroutine 中断。 - 可见性:当一个 goroutine 设置
done为1后,其他 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 完成
}
运行上述代码,你会看到:
- 所有 goroutine 都会尝试调用
Do方法。 - 只有一个 goroutine 会执行传入的函数(打印 "Goroutine X is executing the function")。
- 其他 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 在保证功能正确性的同时,也具有很好的性能表现。

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