本书未发布
Go: sync.Once和sync.Mutex
之前问过这个问题,现在准备再次重温下这个。博客:sync.Once和自己加锁有什么区别吗?
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
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 loadIconsOnce sync.Once
func Icon(name string) string {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
使用说明:
sync.Once无需初始化也行, 只要申明类型即可。下面的也行!
var loadIconsOnce = sync.Once{}
loadIconsOnce.Do(loadIcons) 只会执行一次。对于多协程调用的话,是安全的。
if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) }
这里比较为什么用原子性比较呢?如果改成普通的比较方法会怎么样?
if o.done == 0 { o.doSlow(f) }
在本地将Once拷贝出来,然后改动如下:
func Icon(name string) string {
go loadIconsOnce.Do(loadIcons)
go loadIconsOnce.Do(loadIcons)
go loadIconsOnce.Do(loadIcons)
return icons[name]
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
fmt.Println("atomic")
o.doSlow(f)
}
}
输出如下:
=== RUN TestIcon
atomic
loading icons...
other_test.go:38:
atomic
--- PASS: TestIcon (0.00s)
PASS
可见在并发时,if atomic.LoadUint32(&o.done) == 0
是不能阻挡并发执行到o.doSlow(f)
。接下来很好理解,通过sync.Mutex锁+比较done的值保证单例只会被执行一次。这么做怎么就比直接用sync.Mutex性能好呢?
如下:
var lock sync.Mutex
func Icon(name string) string {
lock.Lock() //加锁
if icons == nil {
loadIcons()
}
lock.Unlock()
return icons[name]
}
每次执行Icon函数都会加锁。但是如果只执行一次呢?如下:
func Init() {
lock.Lock()
defer lock.Unlock()
loadIcons()
}
func Icon(name string) string {
return icons[name]
}
这样就在项目启动时执行一次Init,然后在执行Icon方法才能获取到值。如果Init还正在初始化中,但是已经在调用Icon呢?那么就会出现问题。
通过这样对比,就能发现sync.Once的好处了:
- 方法中使用sync.Once后,无须担心并发多次调用和后续再被调用带来的性能问题。因为无论调多少次只是多了一次done的比较而已。
- 被调用的方法保证一定能执行完才会返回,这样保证了初始化的数据一定会初始化完成再被使用。如果只是sync.Mutex无法做到并发时的安全。
- 写法上的区别,sync.Once可以在初始化和调用时写在一起。