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 mainimport ("fmt""sync""time")func main() {sum := 0var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func() {time.Sleep(100)sum += iwg.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.
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.
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.
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 mainimport ("fmt""sync""time")func main() {sum := 0var wg sync.WaitGroupfor i := 0; i < 10; i++ {value := iwg.Add(1)go func() {time.Sleep(100)sum += valuewg.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.
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.
Free Resources