在阅读Go语言的源代码时,我对src/sync/once.go中的代码有一个疑问:
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
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()
}
}
为什么使用atomic.StoreUint32
,而不是o.done = 1
?它们不等价吗?有什么区别?
在一台弱内存模型的机器上,我们必须使用原子操作(atomic.StoreUint32
)来确保其他goroutine在o.done
被设置为1之前能够观察到f()
的效果吗?
4条答案
按热度按时间v440hwme1#
请记住,除非你是手工编写程序集,否则你不是在机器的内存模型下编程,而是在Go语言的内存模型下编程,这意味着即使原语赋值在你的体系结构中是原子的,Go语言也需要使用原子包来确保在所有支持的体系结构中的正确性。
在互斥锁之外访问
done
标志只需要是安全的,而不是严格有序的,因此可以使用原子操作,而不是总是通过互斥锁获得锁。这是一种优化,使快速路径尽可能高效,允许sync.Once
用于热路径。用于
doSlow
的互斥锁仅用于该函数内部的互斥,以确保在设置done
标志之前,只有一个调用方到达f()
。该标志使用atomic.StoreUint32
写入,因为它可能与atomic.LoadUint32
同时发生,并且在互斥锁保护的临界区之外。与写入(即使是原子写入)同时阅读
done
字段是一种数据竞争。仅仅因为该字段是原子读取的,并不意味着您可以使用正常赋值来写入它,因此首先使用atomic.LoadUint32
检查标志,然后使用atomic.StoreUint32
写入标志在
doSlow
* 中直接读取done
是 * 安全的,因为互斥体保护它不被并发写入。与atomic.LoadUint32
并发阅读值是安全的,因为两者都是读取操作。yyyllmsg2#
在一台弱内存模型的机器上,我们必须使用原子操作(
atomic.StoreUint32
)来确保其他goroutine在o.done
被设置为1之前能够观察到f()
的效果吗?是的,你的思路是对的,但是请注意,即使目标机器有强大的内存模型,只要结果符合Go memory model,Go语言编译器就可以重新排序指令;相反,即使机器内存模型比语言内存模型弱,编译器也必须设置额外的障碍,以使最终代码符合语言规范。
让我们考虑一下没有
sync/atomic
的sync.Once
的实现,为了更容易解释,我们做了一些修改:如果一个goroutine观察到
o.done != 0
,它将返回,因此,函数必须确保f()
发生在任何读操作可以从o.done
观察到1之前。f
并将o.done
设置为1的互斥锁之后发生。因此,write(6)必须有释放语义,read(1)必须有获取语义,因为Go语言不支持获取-读取和释放-存储,所以我们必须求助于
atomic.(Load/Store)Uint32
提供的更强的顺序,即序列一致性。最后一点:由于对不大于一个机器字的内存位置的访问被保证是原子性的,所以这里使用
atomic
与原子性无关,而与同步有关。6qfn3psc3#
cbeh67ev4#
原子操作可以用来同步不同goroutine的执行。
如果没有同步,即使goroutine观测到o.done == 1,也不能保证它会观测到
f()
的影响。