Go语言中的sync
包中提供了一个针对只执行一次场景的解决方案–sync.Once
。
sync.Once
只有一个Do
方法
Go源码
type Once struct {
done uint32 // 标志位
m Mutex
}
// 判断标志位 函数是否已经执行 参数位不为0则表明已执行,直接跳过
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()
}
}
举例
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
以上代码中,当多个goroutine
并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine
都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断icons
不是nil也不意味着变量完成了初始化。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons
的时候不会被其他的goroutine
操作,但是这样做又会引发性能问题。
使用sync.Once
改造的示例代码如下:
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
因为Do
方法只能接收函数类型,所以在使用时往往要和闭包配合使用。
f := func(){
close(ch1)
}
once.Do(f)
// 简写为
once.Do(func(){close(ch1)})