8

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?

Ryan Lyu
  • 4,180
  • 5
  • 35
  • 51

3 Answers3

24

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.

tangrs
  • 9,709
  • 1
  • 38
  • 53
1

Even if the processor's 64 bit load and store are single instructions, you still need atomic.

Consider the writing goroutine is running in cpu1 and the reading gorouting in cpu2. Writing to the variable would first write to the cpu1's cache and it may not put it in the main memory unless the cache space is needed for something else. cpu2 would not see the write at all when it fetches from main memory.

By calling the atomic functions, the data is explicitly written to the main memory and loaded from main memory.

balki
  • 26,394
  • 30
  • 105
  • 151
0

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.

cheng dong
  • 119
  • 5