once
2024年12月21日大约 3 分钟
once
sync.Once
是 Go 标准库中的一个同步工具,用于确保某个函数在并发环境下只执行一次。它通常用于延迟初始化、全局资源的初始化等场景,确保某个特定的操作只会被执行一次,即使有多个协程并发执行。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var once sync.Once
for i := range 10 {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() {
fmt.Println(i)
})
}()
}
wg.Wait()
}
在上面的代码中,总共有 10 个协程,但无论如何,只有一个协程会执行 once.Do()
中的 fmt.Println(i)
。具体是哪一个协程会打印,取决于哪个协程最先到达 once.Do()
。
结构
type Once struct {
done atomic.Uint32
m Mutex
}
它的内部结构非常简单
done
,一个原子值,用于表示是否被执行过m
,互斥锁,用于阻塞其它想要执行的协程
它的原理就是在执行前先加锁,然后更新done
,执行完毕后解锁。
Do
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}
代码整体上非常简单,流程如下:
- 直接加载原子值,如果已经执行过了就直接返回
- 没有执行过,则尝试持有锁,这里可能是多个协程在竞争互斥锁
- 当胜者成功持有锁后,还需要再一次判断
done
的值,因为在你持有锁后,别人可能已经完成加锁-执行-解锁一条龙了,这时你才刚被唤醒。 - 执行目标函数
- 更新
done
值 - 释放锁
小结
sync.Once
是一个非常简洁而高效的同步工具,它确保了并发环境下某个操作只执行一次,从而避免了重复工作或资源的浪费。