什么是sync.Once
在很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只初始化一次service等。Go语言中的sync包中提供了一个针对只执行一次场景的解决方案:sync.Once。
基本使用
假设需要初始化blogController,先声明once:
var blogControllerOnce sync.Once
用once初始化blogController:
func NewBlogController(blogService service.BlogService) *BlogControllerImpl {
blogControllerOnce.Do(func() {
blogController = &BlogControllerImpl{
BlogService: blogService,
}
})
return blogController
}
这样能保证blogController的初始化过程只被执行一次
源码分析
Once结构体定义如下
type Once struct {
done uint32
m Mutex
}
done
是标志位,用来判断方法f
是否被执行过,初始值为0
,当方法f第一次执行完毕时,done
被设为1
m
用来控制不同goroutine同时执行Done
时,保证只有一个goroutine能执行
once.Do:
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
这里用atomic.LoadUint32
获取done
的值,如果为0
,说明还没有初始化过,调用doSlow执行参数中的方法,否则直接退出
先看看该原子读操作:
LoadUint32
方法把一个32位整数读出来,保证其原子性
为什么会有这样的需求?难道读写一个整数不是CPU指令级别能保证的原子操作吗?目前通用的CPU指令能保证原子地读取32/64位整数,该原子操作好像没有必要
但这里是为了兼容不同的计算机体系结构,从语言层面提供一个统一的函数。可能有些CPU不支持原子读取32/64位整数,例如只保存一次读取一个字节的数据,这时语言层面的原子操作就很有必要
同时原子操作还保证原子写后会立即将数据刷新到内存,原子读从内存中读:
现在编译器可能会做优化,在代码运气期间对变量的读写直接访问寄存器,而不写入内存,以提高性能。这在同一个执行上下文是没有问题的,而如果需要跨线程访问,则永远看不到数据的变化
举个例子:
var x int64 = 0
func storeFunc() {
for i := 0; ; i++ {
if i%2 == 0 {
x = 1
} else {
x = 2
}
// time.Sleep(10 * time.Millisecond)
}
}
func main() {
go storeFunc()
for {
time.Sleep(1 * time.Second)
fmt.Printf("%x\n", x)
}
}
- 定义全局遍历x,初始化为0
- 新起goroutine,在storeFunc中不断修改x的值为1或2
- 主goroutine中每隔一秒检查x的值
实验结果为,每次x
的值都为0。这里因为在storeFunc
修改x
太频繁,编译器做了优化,直接在寄存器上操作,而每把结果刷到内存中,若果每次for循环暂停一会,就可以在主goroutine中看到修改的值
因此这里用原子读写操作,保证了可见性和原子性
回到once.Do
,如果没有执行过,就执行doSlow
方法:
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
这是标准的双重检测实现单例模式,加锁后如果o.Done
为0(因为上一步加了锁,单协程情况下本身有原子性,因此不用原子读取),说明还执行过,执行f()
doSlow
中的o.done == 0
这个判读是必须的,因为可能会出现A,B俩个协程都进行了LoadUint32
判断,并且都是true
,如果不进行第二次校验的话,f就会被调用两次,和预期不符
有一个思考点,Do方法中的atomic.LoadUint32(&o.done)
,能够替换成以下代码吗?
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
答案是不可以,因为如果先将done
设为1,再执行f
,如果同时有另一个goroutine进来,发现已经执行完毕了,就使用f执行后的结果,但此时第一个goroutine还没开始执行,会造成程序错误
总结
本文主要对sync.Once进行了源码分析,知道其工作原理,并介绍了原子操作