Beware mutation

Hercules Lemke Merscher - Jan 4 '22 - - Dev Community

Can you spot the problem here?

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

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 Go piece of 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)
}
Enter fullscreen mode Exit fullscreen mode

The same "unexpected" behavior! And it's 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 asynchrounously, turning 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 is never a problem when you have synchronous code, but the moment parallelism is introduced it starts behaving unexpectedly. Even worse if it has to do with concurrency, because 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);
Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode

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.

Concluding

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])
    }
}
Enter fullscreen mode Exit fullscreen mode

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, and this is not the most idiomatic Go code but translates well to other programming languages, which makes it perfect to illustrate the issue.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .