- read

Why Your Go Counter Isn’t Counting Right: sync/atomic Uncovered

Phuong Le (@func25) 56

Why Your Go Counter Isn’t Counting Right: sync/atomic Uncovered

While we can use mutex to shield an integer from simultaneous goroutine modifications, it might not always be the optimal choice.

Phuong Le (@func25)
Level Up Coding
Published in
5 min read16 hours ago

--

Photo by Pierre Bamin on Unsplash

Before we dive deeper into the sync/atomic package, let's take a moment to unpack a piece of code that often trips up many developers

var counter = 0
var wg = sync.WaitGroup{}

func AddCounter() {
defer wg.Done()
counter++
}

func main() {
for i := 0; i < 2000; i++ {
wg.Add(1)
go AddCounter()
}

wg.Wait()
fmt.Println(counter) // ?
}

What number do you anticipate the counter would display?

You might already sense that 2000 isn’t a guaranteed outcome, and that’s the heart of the challenge.

I remember running the code for the first time, expecting a clear 2000, but what I got was 1942, and then on a rerun, 1890… it felt like rolling dice.

Feel free to try it at: https://go.dev/play/p/iXK7nGFRrRZ

At first glance, the counter++ operation might look straightforward, but beneath the surface, it's performing a trio of tasks:

  1. Fetching the current value of counter.
  2. Increasing that value by a single unit.
  3. Storing this refreshed value back into counter.

Imagine two goroutines trying to update the counter at the exact same time.

They could both read the initial value, add to it, and then save the new number. In this scenario, the counter might only increase by one, even though two separate routines tried to bump it up."

Your Fix to the Problem

Remember our discussion about the ‘Go Sync Package: 6 Key Concepts for Concurrency’? And you’re probably leaning towards using a mutex lock right?

Let’s play around with our goroutine and mutex lock a bit: