JavaScript 中的竞争条件与复合赋值

Posted

技术标签:

【中文标题】JavaScript 中的竞争条件与复合赋值【英文标题】:Race condition in JavaScript with compound assignment 【发布时间】:2017-11-04 09:35:52 【问题描述】:

我不是在谈论复杂的比赛条件involving the network 或events。相反,我似乎发现 += 运算符在 V8(Chrome 58 或 Node 8)中不是原子的。

下面的代码旨在并行运行两个所谓的线程。每个“线程”重复调用一个函数,该函数在sleeping 那么多秒后返回其数字参数。结果是summed up 进入累加器。

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
)();

问题是,大约 5 秒后,我希望累加器为 10.3(5 乘以 1 + 1 乘以 5.3)。但是,它是 5.3!

【问题讨论】:

我很好奇实现是如何在幕后进行的,也许在 await 语句期间创建了一个闭包,该语句存储了 await 调用时当前范围内的所有变量。有趣的是,这两个函数似乎完全独立地增加了 acc,就像它们将 acc 放在本地范围而不是全局范围中一样 【参考方案1】:

确实,将acc += await n(c) 行替换为:

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

避免了竞争条件。

我的猜测是 V8 并没有将 n() 结果的 ADD 优化为包含 acc 的内存位置上的 acc += await n(c),而是将其扩展为 acc = acc + await n(c);,以及 @987654328 的初始值@nForever(5.3) 首次调用时为 0。

这对我来说是违反直觉的,尽管不确定 V8 开发人员是否会认为这是一个错误。

【讨论】:

有趣的是,如果我通过 babeljs 网站转换您的原始代码,它实际上会按照您的预期工作,在 5.3 秒后给出 10.3【参考方案2】:

这不是竞争条件,因为您明确使用await 让步执行。

该标准定义复合赋值(如+=)不是原子的:复合赋值的左侧在右侧之前被评估。[1]

因此,如果您的 RHS 以某种方式更改 acc,则更改将被覆盖。最简单的例子:

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

console.log(n);

【讨论】:

感谢您指出标准。它表明我的 assembly language driven mental model of the compound assignment 不正确(准确地说,被第 5 步破坏了)。

以上是关于JavaScript 中的竞争条件与复合赋值的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Java 没有条件与和条件或运算符的复合赋值版本? (&&=, ||=)

JavaScript中赋值运算符的使用

JavaScript -- 操作符和逻辑运算

JavaScript 事件处理的竞争条件?

仅当 JavaScript 中的三元运算符中的条件为真时才赋值

第一百零九节,JavaScript面向对象与原型