6
func resetElectionTimeoutMS(newMin, newMax int) (int, int) {
    oldMin := atomic.LoadInt32(&MinimumElectionTimeoutMS)
    oldMax := atomic.LoadInt32(&maximumElectionTimeoutMS)
    atomic.StoreInt32(&MinimumElectionTimeoutMS, int32(newMin))
    atomic.StoreInt32(&maximumElectionTimeoutMS, int32(newMax))
    return int(oldMin), int(oldMax)
}

我有一个像这样的代码功能。我感到困惑的是:为什么我们需要atomic这里?这是为了防止什么?

谢谢。

4

2 回答 2

15

原子函数以孤立的方式完成任务,其中任务的所有部分似乎都是瞬间发生或根本不发生。

在这种情况下,LoadInt32 和 StoreInt32 确保整数的存储和检索方式不会让加载的人获得部分存储。但是,您需要双方都使用原子函数才能正常运行。raft示例看起来不正确,至少有两个原因。

  1. 两个原子函数不能充当一个原子函数,因此在两行中读取旧函数并设置新函数是一种竞争条件。您可能会阅读,然后其他人设置,然后您设置并且您在设置之前返回先前值的错误信息。

  2. 并非每个访问 MinimumElectionTimeoutMS 的人都在使用原子操作。这意味着在此函数中使用原子实际上是无用的。

这将如何解决?

func resetElectionTimeoutMS(newMin, newMax int) (int, int) {
    oldMin := atomic.SwapInt32(&MinimumElectionTimeoutMS, int32(newMin))
    oldMax := atomic.SwapInt32(&maximumElectionTimeoutMS, int32(newMax))
    return int(oldMin), int(oldMax)
}

这将确保 oldMin 是交换之前存在的最小值。然而,整个函数仍然不是原子的,因为最终结果可能是从未使用 resetElectionTimeoutMS 调用的 oldMin 和 oldMax 对。为此......只需使用锁。

还需要更改每个函数以执行原子加载:

func minimumElectionTimeout() time.Duration {
    min := atomic.LoadInt32(&MinimumElectionTimeoutMS)
    return time.Duration(min) * time.Millisecond
}

我建议您仔细考虑 golang atomic 文档中提到的 VonC 引用:

这些功能需要非常小心才能正确使用。除了特殊的低级应用程序外,最好使用通道或同步包的工具来完成同步。

如果您想了解原子操作,我建议您从http://preshing.com/20130618/atomic-vs-non-atomic-operations/开始。这超出了您的示例中使用的加载和存储操作。但是,原子还有其他用途。go atomic 包概述介绍了一些很酷的东西,例如原子交换(我给出的示例)、比较和交换(称为 CAS)和添加。

我给你的链接中的一个有趣的引用:

众所周知,在 x86 上,如果内存操作数自然对齐,则 32 位 mov 指令是原子指令,否则非原子指令。换句话说,只有当 32 位整数位于恰好是 4 的倍数的地址时,原子性才能得到保证。

换句话说,在当今的通用系统上,您的示例中使用的原子函数实际上是无操作的。它们已经是原子的了!(虽然不能保证它们,如果您需要它是原子的,最好明确指定它)

于 2014-08-27T06:24:15.783 回答
1

考虑到该atomic提供了可用于实现同步算法的低级原子内存原语,我想它旨在用作:

  • MinimumElectionTimeoutMS存储时不被修改oldMin
  • MinimumElectionTimeoutMS在设置为新值时不会被修改newMin

但是,该软件包确实带有警告:

这些功能需要非常小心才能正确使用。
除了特殊的低级应用程序外,最好使用通道或同步包的工具来完成同步。
通过通信共享内存;不要通过共享内存进行通信。

在这种情况下(server.go来自Raft 分布式共识协议),直接在变量上同步可能被认为比Mutex在 all 函数上放置 a 更快。

除了,正如斯蒂芬温伯格的回答所示(赞成),这不是你使用原子的方式。它只确保oldMin在进行交换时是准确的。

请参阅“两个原子样式代码是否sync/atomic.once.go必要? ”中的另一个示例,与“内存模型”有关。


OneOfOne在评论中提到使用原子 CAS 作为自旋锁(非常快的锁定):

BenchmarkSpinL-8            2000            708494 ns/op           32315 B/op       2001 allocs/op
BenchmarkMutex-8            1000           1225260 ns/op           78027 B/op       2259 allocs/op

看:

于 2014-08-27T06:10:20.043 回答