JavaScript 中的数据竞争?

Data Races in JavaScript?

假设我 运行 这段代码。

var score = 0;
for (var i = 0; i < arbitrary_length; i++) {
     async_task(i, function() { score++; }); // increment callback function
}

从理论上讲,我知道这会导致数据竞争,并且两个线程试图同时递增可能会导致单次递增,但是众所周知,nodejs(和 javascript)是单线程的。我能保证 score 的最终值等于 arbitrary_length 吗?

函数的两次调用不能同时发生(b/c 节点是单线程的)所以这不会成为问题。唯一的问题是在某些情况下 async_task(..) 会丢弃回调。但是,例如,如果 'async_task(..)' 只是用给定的函数调用 setTimeout(..),那么是的,每个调用都会执行,它们永远不会相互冲突,并且 'score' 将具有预期的值,'arbitrary_length',最后.

当然,'arbitrary_length' 不能大到耗尽内存,或溢出保存这些回调的任何集合。但是没有线程问题。

节点使用事件循环。您可以将其视为队列。所以我们可以假设,您的 for 循环将 function() { score++; } 回调放入此队列 arbitrary_length 次。之后js引擎一个一个运行这些,每次增加score。所以是的。唯一的例外情况是未调用回调或从其他地方访问 score 变量。

实际上,您可以使用此模式并行执行任务、收集结果并在每个任务完成时调用单个回调。

var results = [];
for (var i = 0; i < arbitrary_length; i++) {
     async_task(i, function(result) {
          results.push(result);
          if (results.length == arbitrary_length)
               tasksDone(results);
     });
}

Am I guaranteed that the final value of score will be equal to arbitrary_length?

是的,只要所有async_task()个调用调用回调一次且仅调用一次,就可以保证score的最终值等于arbitrary_length。

正是 Javascript 的单线程特性保证了永远不会有两个 Javascript 运行ning 完全同时出现。相反,由于 Javascript 在浏览器和 node.js 中的事件驱动特性,一段 JS 运行s 完成,然后下一个事件从事件队列中拉出并触发一个也将 运行 完成的回调。

没有中断驱动 Javascript 这样的东西(其中一些回调可能会中断当前 运行ning 的其他 Javascript 片段)。一切都通过事件队列序列化。这是一个巨大的简化,并防止了很多棘手的情况,否则当您有多个线程 运行ning 并发或中断驱动的代码时,安全编程将需要大量工作。

还有一些并发问题需要关注,但它们更多地与多个异步回调都可以访问的共享状态有关。虽然在任何给定时间只有一个人会访问它,但仍然有可能一段包含多个异步操作的代码在处于多个异步操作的中间时将某些状态留在 "in between" 状态一些其他异步操作可以 运行 并可以尝试访问该数据的点。

您可以在此处阅读有关 Javascript 的事件驱动性质的更多信息:How does JavaScript handle AJAX responses in the background? 并且该答案还包含许多其他参考资料。

另一个讨论可能的共享数据竞争条件的类似答案:

其他一些参考资料:

how do I prevent event handlers to handle multiple events at once in javascript?

Do I need to be concerned with race conditions with asynchronous Javascript?


让您了解 Javascript 中可能发生的并发问题(即使没有线程也没有中断,这是我自己的代码中的示例。

我有一个 Raspberry Pi node.js 服务器控制我家的阁楼风扇。每 10 秒它检查两个温度探测器,一个在阁楼内,一个在屋外,并决定如何控制风扇(通过继电器)。它还记录可以在图表中显示的温度数据。每小时一次,它会将在内存中收集的最新温度数据保存到一些文件中,以便在断电或服务器崩溃时持久保存。该保存操作涉及一系列异步文件写入。这些异步写入中的每一个都会将控制权交还给系统,然后在调用异步回调信号完成时继续。因为这是一个低内存系统并且数据可能会占用可用 RAM 的很大一部分,所以数据在写入之前不会复制到内存中(这根本不切实际)。所以,我正在将内存中的实时数据写入磁盘。

在任何这些异步文件 I/O 操作期间的任何时候,在等待回调以表示涉及的许多文件写入完成时,我在服务器中的一个计时器可能会触发,我会收集一组新的温度数据,这将尝试修改我正在编写的内存数据集。这是一个等待发生的并发问题。如果它在我写入一部分数据时更改了数据,并且在写入其余部分之前等待该写入完成,那么写入的数据很容易最终被破坏,因为我将写出一部分数据,即数据将从我下面被修改,然后我将尝试写出更多数据而没有意识到它已被更改。那是一个并发问题。

我实际上有一个 console.log() 语句,它在我的服务器上发生此并发问题时明确记录(并由我的代码安全处理)。它每隔几天在我的服务器上发生一次。我知道它就在那里,而且是真的。

有很多方法可以解决这些类型的并发问题。最简单的方法是在内存中复制所有数据,然后写出副本。因为没有线程或中断,所以在内存中制作副本不会发生并发(不会在副本中间屈服于异步操作而产生并发问题)。但是,在这种情况下这是不切实际的。所以,我实现了一个队列。每当我开始写作时,我都会在管理数据的对象上设置一个标志。然后,只要系统想要在设置该标志时添加或修改存储数据中的数据,这些更改就会进入队列。设置该标志时不会触及实际数据。当数据已安全写入磁盘后,将重置标志并处理排队的项目。安全地避免了任何并发问题。


因此,这是您必须关注的并发问题示例。 Javascript 的一个很好的简化假设是,一块 Javascript 将 运行 完成而不会中断任何线程,只要它不故意 return 控制回系统。这使得处理上述并发问题变得非常容易,因为除非您有意识地将控制权交还给系统,否则您的代码永远不会被中断。这就是为什么我们不需要互斥量和信号量以及我们自己的 Javascript 中的其他类似东西。如果需要,我们可以像上面描述的那样使用简单的标志(只是一个常规的 Javascript 变量)。


在 Javascript 的任何完全同步片段中,您永远不会被其他 Javascript 打断。 Javascript 的同步片段将 运行 在处理事件队列中的下一个事件之前完成。这就是 Javascript 是一种 "event-driven" 语言的意思。例如,如果您有以下代码:

 console.log("A");
 // schedule timer for 500 ms from now
 setTimeout(function() {
     console.log("B");
 }, 500);

 console.log("C");

 // spin for 1000ms
 var start = Date.now();
 while(Data.now() - start < 1000) {}

 console.log("D");

您将在控制台中获得以下内容:

A
C
D
B

在当前的 Javascript 运行 秒完成之前无法处理计时器事件,即使它可能比这更早添加到事件队列中。 JS 解释器的工作方式是,它 运行 控制当前的 JS,直到它 return 将控制权交还给系统,然后(并且只有那时),它从事件队列中获取下一个事件并调用与该事件关联的回调。

这是幕后事件的顺序。

  1. 本JS启动运行ning.
  2. console.log("A")输出。
  3. 从现在起 500 毫秒后安排了一个计时器事件。计时器子系统使用本机代码。
  4. console.log("C")输出。
  5. 代码进入自旋循环。
  6. 在自旋循环中途的某个时间点,先前设置的计时器已准备好启动。由解释器实现来决定它是如何工作的,但最终结果是一个定时器事件被插入到 Javascript 事件队列中。
  7. 自旋循环结束。
  8. console.log("D")输出。
  9. 这一段 Javascript 结束,return 控制权交还给系统。
  10. Javascript 解释器看到 Javascript 的当前片段已完成,因此它检查事件队列以查看是否有任何未决事件等待 运行。它找到计时器事件和与该事件关联的回调并调用该回调(开始新的 JS 执行块)。该代码以 运行ning 开头,并输出 console.log("B")
  11. 那个 setTimeout() 回调完成执行,解释器再次检查事件队列以查看是否有任何其他事件准备好 运行。

我确实认为其他查看此内容的人值得注意,您的代码中有一个常见错误。对于变量 i,在将其传递到 async_task() 之前,您需要使用 let 或重新分配给另一个变量。当前的实现将导致每个函数获得 i 的最后一个值。