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.
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:
- Fetching the current value of
counter
. - Increasing that value by a single unit.
- 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: