在阅读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()
}
}
Run Code Online (Sandbox Code Playgroud)
为什么ataomic.StoreUint32使用,而不是说o.done = 1?这些不是等价的吗?有什么区别?
atomic.StoreUint32在弱内存模型的机器上观察到o.done设置为1之前,我们是否必须使用原子操作()来确保其他goroutine可以观察到“f()”的效果?
请记住,除非您手动编写程序集,否则您不是针对机器的内存模型进行编程,而是针对 Go 的内存模型进行编程。这意味着即使原始分配对于您的架构来说是原子的,Go 也需要使用原子包来确保所有支持的架构的正确性。
在互斥体之外访问done标志只需要安全,不需要严格排序,因此可以使用原子操作而不是总是用互斥体获取锁。这是一种优化,旨在使快速路径尽可能高效,从而允许sync.Once在热路径中使用。
用于的互斥体仅用于该函数内的互斥,以确保在设置标志之前doSlow只有一个调用者能够实现这一目的。该标志使用 写入,因为它可能与互斥锁保护的临界区之外同时发生。f()doneatomic.StoreUint32atomic.LoadUint32
done与写入(甚至原子写入)同时读取字段是一种数据竞争。仅仅因为该字段是原子读取的,并不意味着您可以使用正常的赋值来写入它,因此首先检查该标志并atomic.LoadUint32写入atomic.StoreUint32
doneinside的直接读取doSlow 是安全的,因为它受到互斥体的并发写入保护。同时读取值atomic.LoadUint32是安全的,因为两者都是读操作。
| 归档时间: |
|
| 查看次数: |
282 次 |
| 最近记录: |