JavaScript中的数据竞争?

让我们假设我运行这段代码。

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

从理论上讲,我知道这表示一个数据竞赛,并且两个线程同时尝试增加可能会导致单个增量,但是,nodejs(和javascript)被称为单线程。 我保证得分的最终值将等于任意长度?

节点使用事件循环。 你可以把这看成一个队列。 所以我们可以假设你的for循环把function() { score++; } function() { score++; }在此队列上callbackarbitrary_length时间。 之后,js引擎逐一运行,每次都增加score 。 所以是的。 唯一的例外是,如果未调用callback或从其他地方访问scorevariables。

其实你可以使用这种模式来完成任务并行,收集结果,并在每个任务完成时调用一个callback。

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

我保证得分的最终值将等于任意长度?

是的,只要所有的async_task()调用async_task()调用一次callback函数,就可以保证得分的最终值等于任意长度。

这是Javascript的单线程性质,它保证从来没有两个Javascript脚本在同一时间运行。 相反,由于JavaScript在浏览器和node.js中的事件驱动性质,一个JS运行完成,然后下一个事件从事件队列中拉出,并触发一个callback,这个callback也将运行完成。

没有中断驱动的Javascript(其中一些callback可能会中断当前正在运行的其他一些Javascript)。 一切都通过事件队列进行序列化。 这是一个巨大的简化,可以防止很多严格的情况,否则当您有多个线程同时运行或中断驱动的代码时,可能会有很多工作需要安全编程。

还有一些并发问题需要关注,但是他们更多的是与多个asynchronouscallback都可以访问的共享状态有关。 虽然在任何时候只有一个人会访问它,但是包含多个asynchronous操作的代码仍然有可能在某个状态处于“在…之间”的状态,而处于一个asynchronous操作的中间点一些其他的asynchronous操作可能会运行,并可能试图访问该数据。

您可以在这里阅读更多关于JavaScript的事件驱动本质: JavaScript如何在后台处理AJAX响应? 这个答案也包含了许多其他的参考。

另一个类似的答案讨论了可能的共享数据争用条件的种类: 此代码是否会导致socket io中的争用情况?

其他一些参考:

如何防止事件处理程序在JavaScript中一次处理多个事件?

我需要关心asynchronousJavascript的竞争条件吗?

JavaScript – 何时调用堆栈变为“空”?

有多个并发请求的Node.js服务器,它是如何工作的?


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

我有一个树莓派node.js服务器,控制我家的阁楼粉丝。 每隔10秒钟检查两个温度探头,一个在阁楼内,一个在屋外,决定如何控制风扇(通过继电器)。 它还logging可以在图表中显示的温度数据。 每隔一小时,它会将在内存中收集的最新温度数据保存到某些文件中,以便在停电或服务器崩溃的情况下保持持久性。 该保存操作涉及一系列asynchronous文件写入。 这些asynchronous写入中的每一个都会将控制权交还给系统,然后在asynchronouscallback被称为信号完成时继续。 由于这是一个低内存系统,数据可能会占用可用RAM的很大一部分,所以在写入之前数据不会被复制到内存中(这是不实际的)。 所以,我正在将内存中的数据写入磁盘。

在这些asynchronous文件I / O操作中的任何时候,在等待callback来表示完成涉及到的多个文件写入时,服务器中的一个定时器可能会触发,我会收集一组新的温度数据,这将试图修改我正在写入的内存数据集。 这是一个等待发生的并发问题。 如果它在写入数据的同时更改了数据,并在写入数据之前等待写入完成,那么写入的数据很容易被损坏,因为我将写出数据的一部分数据将从下面被修改,然后我会尝试写出更多的数据,而不会意识到已经被修改了。 这是一个并发问题。

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

解决这些types的并发问题有很多方法。 最简单的办法是在所有数据的内存中进行复制,然后写出副本。 因为没有线程或中断,所以在内存中创build一个副本是不安全的(在副本中间不会产生asynchronous操作来产生并发问题)。 但是,在这种情况下这是不实际的。 所以,我实现了一个队列。 每当我开始写作时,我都会在pipe理数据的对象上设置一个标志。 然后,任何时候当系统想要在存储的数据中添加或修改数据时,这些改变就进入一个队列。 该标志被设置时,实际的数据不会被触摸。 当数据已被安全地写入磁盘时,该标志被重置并且排队的项目被处理。 任何并发问题都被安全地避免了。


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


在任何完全同步的Javascript中,您将永远不会被其他Javascript中断。 在处理事件队列中的下一个事件之前,同步的一段Javascript会运行完成。 Javascript是一种“事件驱动”语言。 作为一个例子,如果你有这样的代码:

  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,直到它将控制权返回给系统,然后(并且仅在此时),它从事件队列中获取下一个事件,并调用与该事件关联的callback。

这里是封面下的一系列事件。

  1. 这JS开始运行。
  2. 输出console.log("A")
  3. 计时器事件从现在开始计划500毫秒。 计时器子系统使用本地代码。
  4. 输出console.log("C")
  5. 代码进入旋转循环。
  6. 在部分时间点通过旋转循环,先前设置的计时器准备好发射。 由解释器实现决定如何工作,但最终的结果是定时器事件被插入到Javascript事件队列中。
  7. 旋转循环结束。
  8. 输出console.log("D")
  9. 这段JavaScript完成并将控制权还给系统。
  10. Javascript解释器发现当前的一段Javascript已经完成,因此它会检查事件队列以查看是否有任何正在等待运行的未决事件。 它find定时器事件和与该事件相关的callback,并调用该callback(开始一个新的JS执行块)。 该代码开始运行,并输出console.log("B")
  11. setTimeout()callback完成执行,解释器再次检查事件队列以查看是否有其他准备运行的事件。

没有两个函数调用可以同时发生(b / c节点是单线程的),所以不会是一个问题。 唯一的问题是如果在某些情况下,async_task(..)会丢弃callback。 但是,例如,如果async_task()只是用给定的函数调用setTimeout(..),那么是的,每个调用都会执行,它们永远不会相互冲突,'score'会得到预期的值,'arbitrary_length',结尾。

当然,“任意长度”不能耗尽内存,或者不pipe收集哪些callback都会溢出。 没有线程问题。