原子函数以孤立的方式完成任务,其中任务的所有部分似乎都是瞬间发生或根本不发生。
在这种情况下,LoadInt32 和 StoreInt32 确保整数的存储和检索方式不会让加载的人获得部分存储。但是,您需要双方都使用原子函数才能正常运行。raft示例看起来不正确,至少有两个原因。
两个原子函数不能充当一个原子函数,因此在两行中读取旧函数并设置新函数是一种竞争条件。您可能会阅读,然后其他人设置,然后您设置并且您在设置之前返回先前值的错误信息。
并非每个访问 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 的倍数的地址时,原子性才能得到保证。
换句话说,在当今的通用系统上,您的示例中使用的原子函数实际上是无操作的。它们已经是原子的了!(虽然不能保证它们,如果您需要它是原子的,最好明确指定它)