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!
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:Indeed, after replacing the
acc += await n(c)
line with:the race condition was avoided.
My guess is V8 didn't optimize
acc += await n(c)
to an ADD of then()
result over the memory location containingacc
, but rather expanded it toacc = acc + await n(c);
, and the initial value ofacc
whennForever(5.3)
was first called, was 0.This is counter-intuitive to me, though not sure the V8 developers would consider it a bug.