执行原子加载和存储操作

huangapple go评论119阅读模式
英文:

go atomic Load and Store

问题

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)
}

我得到了一个类似这样的Go代码函数。
我感到困惑的是:为什么我们在这里需要使用atomic?这是为了防止什么?

谢谢。

英文:
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)
}

I got a go code function like this.
The thing I am confused is: why do we need atomic here? What is this preventing from?

Thanks.

答案1

得分: 16

原子函数以一种隔离的方式完成任务,任务的所有部分似乎同时发生或根本不发生。

在这种情况下,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
}

我建议你仔细考虑VonC在golang原子文档中提到的引用:

这些函数需要非常小心地使用。除了特殊的低级应用程序外,最好使用通道或sync包的功能进行同步。

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

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

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

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

英文:

Atomic functions complete a task in an isolated way where all parts of the task appear to happen instantaneously or don't happen at all.

In this case, LoadInt32 and StoreInt32 ensure that an integer is stored and retrieved in a way where someone loading won't get a partial store. However, you need both sides to use atomic functions for this to function correctly. The raft example appears incorrect for at least two reasons.

  1. Two atomic functions do not act as one atomic function, so reading the old and setting the new in two lines is a race condition. You may read, then someone else sets, then you set and you are returning false information for the previous value before you set it.

  2. Not everyone accessing MinimumElectionTimeoutMS is using atomic operations. This means that the use of atomics in this function is effectively useless.

How would this be fixed?

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

This would ensure that oldMin is the minimum that existed before the swap. However, the entire function is still not atomic as the final outcome could be an oldMin and oldMax pair that was never called with resetElectionTimeoutMS. For that... just use locks.

Each function would also need to be changed to do an atomic load:

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

I recommend you carefully consider the quote VonC mentioned from the golang atomic documentation:

>These functions require great care to be used correctly. Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package.

If you want to understand atomic operations, I recommend you start with http://preshing.com/20130618/atomic-vs-non-atomic-operations/. That goes over the load and store operations used in your example. However, there are other uses for atomics. The go atomic package overview goes over some cool stuff like atomic swapping (the example I gave), compare and swap (known as CAS), and Adding.

A funny quote from the link I gave you:

>it’s well-known that on x86, a 32-bit mov instruction is atomic if the memory operand is naturally aligned, but non-atomic otherwise. In other words, atomicity is only guaranteed when the 32-bit integer is located at an address which is an exact multiple of 4.

In other words, on common systems today, the atomic functions used in your example are effectively no-ops. They are already atomic! (They are not guaranteed though, if you need it to be atomic, it is better to specify it explicitly)

答案2

得分: 1

考虑到atomic包提供了低级原子内存原语,用于实现同步算法,我认为它的用途应该是:

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

但是,该包也带有以下警告:

使用这些函数需要非常小心。除了特殊的低级应用程序外,最好使用通道或sync包的功能进行同步。通过通信共享内存,而不是通过共享内存进行通信。

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

但是,正如Stephen Weinberg的答案所示(已获赞),这不是使用原子操作的方式。它只确保在交换过程中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

请参阅:

  • sync/spinlock.go
  • sync/spinlock_test.go
英文:

Considering that the package atomic provides low-level atomic memory primitives useful for implementing synchronization algorithms, I suppose it was intended to be used as:

  • MinimumElectionTimeoutMS isn't modified while being stored in oldMin
  • MinimumElectionTimeoutMS isn't modified while being set to a new value newMin.

But, the package does come with the warning:

> These functions require great care to be used correctly.
Except for special, low-level applications, synchronization is better done with channels or the facilities of the sync package.
Share memory by communicating; don't communicate by sharing memory.

In this case (server.go from the Raft distributed consensus protocol), synchronizing directly on the variable might be deemed faster than putting a Mutex on the all function.

Except, as Stephen Weinberg's answer illustrate (upvoted), this isn't how you use atomic. It only makes sure that oldMin is accurate while doing the swap.

See another example at "Is the two atomic style code in sync/atomic.once.go necessary?", in relation with the "memory model".


OneOfOne mentions in the comments using atomic CAS as a spinlock (very fast locking):

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

See:

huangapple
  • 本文由 发表于 2014年8月27日 13:37:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/25519636.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定