atomic.LoadInt64 is not printing value in correct order

I have implemented atomic.LoadInt64 in a way for what I am expecting it will print value in serial order. But it is printing random value. What’s wrong here?
Since I do not understand atomic in deep so what is the concept I am missing here to implement it correctly?

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var wg sync.WaitGroup

var counter int64

func concurrencyGenerator() {

	gs := 1000

	wg.Add(gs)

	for i := 0; i < gs; i++ {
		go func() {

			atomic.AddInt64(&counter, 1)
			fmt.Println("Value : ", atomic.LoadInt64(&counter))

			wg.Done()
		}()
	}

}

func main() {

	concurrencyGenerator()
	wg.Wait()

	fmt.Println(counter)
}

Go

I mean no offence here, but when it comes to atomics, I have to ask: If you do not have a strong understanding of atomics, why are you using them? If it’s to learn atomics, then disregard my question, but I wanted to make sure there’s not a problem that you have and you think atomics are the solution before understanding how they’re supposed to work. Don’t fall for the ol’ Law of the instrument cognitive bias!

Atomics do not synchronize by themselves. They just ensure that operations execute completely, or in the case of operations that can fail, a failure is as if the operation never happened so you can try again without data inconsistencies.

If you want accesses to the counter to run in sequence, then don’t use concurrency and just use a loop. The whole point of parallelism/concurrency is to have separate executions running at the same time (so task #1 runs the same time as #2 and therefore either could complete in any order).

Even though it’s for C++, I highly recommend Fedor Pikus’ CppCon videos on atomics/lock-free/performance:

I missed something in my explanation:

Your two uses of atomic functions:

  • atomic.AddInt64(&counter, 1)
  • atomic.LoadInt64(&counter)

Are two separate atomic operations and any amount of time could have passed between these two operations. The call to fmt.Println probably eventually gets to an OS system call to write the output to the terminal (or file, etc.) which could cause the Go runtime to execute the operation on some OS thread several microseconds (and many increments of counter) later.

You could instead do:

fmt.Println("Value : ", atomic.AddInt64(&counter, 1))

But that’s not going to change that the goroutine scheduler is going to move around your 1000 goroutines and execute them in a nondeterministic order.