'Golang: what is atomic read used for?

Here we have a go case provided by Go by Example, to explain the atomic package.

https://gobyexample.com/atomic-counters

package main

import "fmt"
import "time"
import "sync/atomic"

func main() {

    var ops uint64

    for i := 0; i < 50; i++ {
        go func() {
            for {
                atomic.AddUint64(&ops, 1)

                time.Sleep(time.Millisecond)
            }
        }()
    }

    time.Sleep(time.Second)

    opsFinal := atomic.LoadUint64(&ops) // Can I replace it?
    fmt.Println("ops:", opsFinal)
}

For atomic.AddUnit64, it's straightforward to understand.

Question1

Regarding read operation, why is it necessary to use atomic.LoadUnit, rather than read this counter directly?

Question2

Can I replace the last two lines with the following lines?

Before

    opsFinal := atomic.LoadUint64(&ops) // Can I replace it?
    fmt.Println("ops:", opsFinal)

After

    opsFinal := ops
    fmt.Println("ops:", opsFinal)

Question3

Are we worrying about this scenario?

  1. CPU loads the data from memory
  2. CPU manipulates data
  3. Write data back to memory. Even though this step is fast, but it still takes time.

When CPU doing step3, another goroutine may read incomplete and dirty data from memory. So use atomic.LoadUint64 could avoid this kind of problem?

Reference

Are reads and writes for uint8 in golang atomic?

go


Solution 1:[1]

It's necessary to use atomic.LoadUint64 because there's no guarantee that the := operator does an atomic read.

For an example, consider a theoretical case where atomic.AddUint64 is implemented as follows:

  1. Take a lock.
  2. Read the lower 32 bits.
  3. Read the upper 32 bits.
  4. Add the number to the lower 32 bits.
  5. Add carry-out of the first operation to the upper 32 bits.
  6. Write the lower 32 bits.
  7. Write the upper 32 bits.
  8. Release lock.

If you do not use atomic.LoadUint64, you could be reading an intermediary result between step 6 and 7.

On certain platforms (e.g. older ARM processors without native support for 64 bit integer operations), it may very well be implemented in the way described above.

The same applies for other sized integers/pointers as well. The exact behaviour will depend on the implementation of the atomic package and the CPU/memory architecture that the program is running on.

Solution 2:[2]

according to golang memory model:

  1. every read will act like it really do a memory load(wont be optimized to a const val after first load, like in c++ O3?).
  2. in 64-bit arch, memory load/write will operate at 64-bit once, so two 32bit in a uint64 will always be observed/write at the same time like atomic
  3. compiler might reorder read/write if it did not change the behavior in one goroutine. means read/write of different value might happens in different order in different goroutine.

so, yes, your code's replace is correct, you can do that.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 cheng dong