让我们假设我运行这段代码。
var score = 0;
for (var i = 0; i < arbitrary_length; i++) {
async_task(i, function() { score++; }); // increment callback function
}
理论上我理解这会产生数据竞争,并且两个尝试同时递增的线程可能会导致单个增量,但是,nodejs(和javascript)已知是单线程的。我保证得分的最终值将等于arbitrary_length吗?
答案 0 :(得分:2)
我保证得分的最终值等于 arbitrary_length?
是的,只要所有async_task()
次调用只调用一次回调,就可以保证得分的最终值等于arbitrary_length。
Javascript的单线程特性保证了在同一时间永远不会运行两个Javascript。相反,由于浏览器和node.js中Javascript的事件驱动特性,一个JS运行完成,然后从事件队列中拉出下一个事件,并触发一个也将运行完成的回调。
没有中断驱动的Javascript(其中一些回调可能会中断当前正在运行的其他一些Javascript)。一切都通过事件队列序列化。这是一个巨大的简化,可以防止很多困难的情况,当你有多个线程同时运行或中断驱动代码时,这些情况可能会安全地编程。
仍然需要关注一些并发问题,但它们更多地与多个异步回调都可以访问的共享状态有关。虽然在任何给定时间只有一个人会访问它,但是包含多个异步操作的一段代码仍然可能会在&#34;之间留下一些状态。当一个其他异步操作可以运行并且可能尝试访问该数据时,它处于几个异步操作的中间。
您可以在此处详细了解Javascript的事件驱动特性:How does JavaScript handle AJAX responses in the background?,该答案还包含许多其他参考资料。
另一个类似的答案讨论了可能的共享数据竞争条件:Can this code cause a race condition in socket io?
其他一些参考文献:
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 - When exactly does the call stack become "empty"?
Node.js server with multiple concurrent requests, how does it work?
为了让您了解Javascript中可能发生的并发问题(即使没有线程也没有中断,这是我自己代码中的一个示例。
我有一个Raspberry Pi node.js服务器,用于控制我家中的阁楼粉丝。它每10秒检查两个温度探头,一个位于阁楼内,另一个位于房屋外,并决定如何控制风扇(通过继电器)。它还记录可以在图表中显示的温度数据。每小时一次,它会将内存中收集的最新温度数据保存到某些文件中,以便在断电或服务器崩溃时保持持久性。该保存操作涉及一系列异步文件写入。这些异步写入中的每一个都将控制权返回给系统,然后在异步回调被称为信号完成时继续。因为这是一个低内存系统,并且数据可能占据可用RAM的很大一部分,所以在写入之前数据不会被复制到内存中(这根本不可行)。因此,我将实时内存数据写入磁盘。
在任何这些异步文件I / O操作期间的任何时候,在等待回调以表示所涉及的许多文件写入完成时,服务器中的一个计时器可能会触发,我会收集一个新的一组温度数据,它将尝试修改我在写入过程中的内存数据集。这是一个等待发生的并发问题。如果它在我写完部分数据的同时更改数据并且在写入其余部分之前等待写入完成,那么写入的数据很容易被破坏,因为我会写出一部分数据,数据将从我下面进行修改,然后我将尝试写出更多数据而不会意识到它已被更改。这是一个并发问题。
我实际上有一个console.log()
语句,在我的服务器上发生此并发问题时显式记录(并由我的代码安全处理)。它每隔几天在我的服务器上发生一次。我知道它在那里而且它是真实的。
有很多方法可以解决这些类型的并发问题。最简单的方法是在内存中复制所有数据,然后写出副本。因为没有线程或中断,所以在内存中制作副本对于并发是安全的(在副本中间不会产生异步操作来创建并发问题)。但是,在这种情况下,这并不实际。所以,我实现了一个队列。每当我开始写作时,我都会在管理数据的对象上设置一个标志。然后,只要系统想要在设置该标志时添加或修改存储数据中的数据,这些更改就会进入队列。设置该标志时不会触摸实际数据。将数据安全写入磁盘后,将重置标志并处理排队的项目。安全地避免了任何并发问题。
因此,这是您必须关注的并发问题的一个示例。使用Javascript的一个很好的简化假设是,只要它没有故意将控制权返回给系统,一段Javascript就会运行完成而没有任何线程被中断。这使得处理并发问题如上所述很多,很容易,因为除非你有意识地将控制权交还给系统,否则你的代码永远不会被中断。这就是为什么我们在自己的Javascript中不需要互斥锁和信号量以及其他类似的东西。我们可以使用简单的标志(只是常规的Javascript变量),如上所述,如果需要的话。
在任何完全同步的Javascript中,您永远不会被其他Javascript打断。在处理事件队列中的下一个事件之前,同步的Javascript将运行完成。这就是Javascript是一个&#34;事件驱动的&#34;语言。作为一个例子,如果你有这个代码:
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,直到它将控制权返回给系统然后(并且只有那时),它从事件队列中获取下一个事件并调用与该事件相关的回调。
此处列出了一系列事件。
console.log("A")
。console.log("C")
。console.log("D")
。console.log("B")
。 setTimeout()
回调完成执行,解释器再次检查事件队列以查看是否还有其他事件可以运行。答案 1 :(得分:1)
没有两个函数调用可以同时发生(b / c节点是单线程的),这样就不会有问题了。唯一的问题是ifin某些情况下async_task(..)会丢弃回调。但是,例如,&#39; async_task(..)&#39;只是用给定的函数调用setTimeout(..),然后是的,每个调用都会执行,它们永远不会相互冲突,并且得分&#39;最终将具有预期的值,#arbitrary_length&#39;。
当然,&#39; arbitrary_length&#39;不能耗尽内存,或溢出任何收集这些回调的集合。但是没有线程问题。
答案 2 :(得分:1)
Node使用事件循环。您可以将其视为队列。所以我们可以假设你的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);
});
}
答案 3 :(得分:0)
我认为值得其他人注意的是,您的代码中有一个常见错误。对于变量i,您需要先使用let或将其重新分配给另一个变量,然后再将其传递给async_task()。当前的实现将导致每个函数获得i的最后一个值。