Trusted answers to developer questions

Beware mutation

Free System Design Interview Course

Many candidates are rejected or down-leveled due to poor performance in their System Design Interview. Stand out in System Design Interviews and get hired in 2024 with this popular free course.

Can you spot the problem here?

let sum = 0;
for (i = 0; i < 10; i++) {
setTimeout(() => (sum += i), 100);
}
setTimeout(() => console.log(sum), 1000);

It’s supposed to print the sum of 0 to 9, but instead, it’s printing 100.

Well, it’s not an exclusivity of Javascript. Have a look at this piece of GO code:

package main
import (
"fmt"
"sync"
"time"
)
func main() {
sum := 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
time.Sleep(100)
sum += i
wg.Done()
}()
}
wg.Wait()
fmt.Println(sum)
}

The same “unexpected” behavior! You may find that this is also the case for many other programming languages, give it a try with your favorite one.

Have you already grasped what’s happening here? You may have already identified the issue, but in case not, try to think a little bit before proceeding if you like to solve this kind of thing.

Unexpected 100?

Is it really unexpected? It’s not, but sometimes it plays a trick on us!

If you didn’t grasp what’s happening here yet, let me show you. The for loop uses a variable i that increases by 1 until it reaches 10. The loop code block is using the value of the variable to increase the sum, however, it’s being done asynchronously, which turns it into a recipe for disaster. The code block has a sleep call, and since the loop goes much faster, by the time the variable sum is increased the variable i got mutated and is now storing the last value, 10. This value is then summed up 10 times.

Beware mutation

This problem never occurs when you have synchronous code, but the moment parallelism is introduced, it starts behaving unexpectedly. Even worse, if it has to do with concurrency, that’s when race conditions come to bite us.

An easy fix

Don’t fear! It’s quite easy to fix this kind of thing:

let sum = 0;
for (i = 0; i < 10; i++) {
const value = i;
setTimeout(() => (sum += value), 100);
}
setTimeout(() => console.log(sum), 1000);
package main
import (
"fmt"
"sync"
"time"
)
func main() {
sum := 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
value := i
wg.Add(1)
go func() {
time.Sleep(100)
sum += value
wg.Done()
}()
}
wg.Wait()
fmt.Println(sum)
}

A simple local variable will suffice to avoid the problem of sharing a mutable variable since now every block will have a copy of the value itself.

Conclusion

My previous example is a silly case. Who wants to sum the loop counter, right? But it’s not so uncommon to see code written like this:

for i := 0; i < 100; i++ {
	go func() {
		sendEmail(users[i])
	}
}

You can change the sendEmail by any other function call doing I/O that you’d like to do in parallel to make it more efficient. This is not the most idiomatic Go code, but it translates well to other programming languages, which makes it perfect for illustrating the issue.

RELATED TAGS

programming
software engineering
distributed systems

CONTRIBUTOR

Hercules Merscher
Attributions:
  1. undefined by undefined
Did you find this helpful?