Race condition in JavaScript with compound assignment

I’m not talking about complex race conditions involving the network or events. Rather, I seem to have found out that the += operator is not atomic in V8 (Chrome 58, or Node 8).

The code below aims to run two so-called threads in parallel. Each “thread” calls repeatedly a function that returns its number parameter after sleeping that many seconds. The results are summed up into an accumulator.

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Return the passed number after sleeping that many seconds
async function n(c) {
  await sleep(c * 1000);
  console.log('End', c);
  return c;
}

let acc = 0;  // global

// Call n repeatedly and sum up results
async function nForever(c) {
  while (1) {
    console.log('Calling', c);
    acc += await n(c);  // += not atomic?!
    console.log('Acc', acc);
  }
}

(async function() {
  // parallel repeated calls
  nForever(1);
  nForever(5.3);  // .3 for sanity, to avoid overlap with 1 * 5
})();

The problem is that after ~5 seconds, I’d expect the accumulator to be 10.3 (5 times 1 + 1 times 5.3). However, it’s 5.3!

+= not atomic

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

This is not a race condition, because you are explicitly yielding the execution using await.

The standard defines that a compound assignment such as += is not atomic: The left-hand-side of a compound assignment is evaluated before the right-hand-side.[1]

So if your RHS changes acc somehow, the changes will be overwritten. Most simple example:

var n = 1;
n += (function () {
    n = 2;
    return 0;
})();

console.log(n);

Solution 2

Indeed, after replacing the acc += await n(c) line with:

const ret = await n(c); acc += ret;

the race condition was avoided.

My guess is V8 didn’t optimize acc += await n(c) to an ADD of the n() result over the memory location containing acc, but rather expanded it to acc = acc + await n(c);, and the initial value of acc when nForever(5.3) was first called, was 0.

This is counter-intuitive to me, though not sure the V8 developers would consider it a bug.

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply