[英]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)已知是单线程的。 我能保证分数的最终值将等于任意长度吗?
我能保证分数的最终值将等于任意长度吗?
是的,只要所有async_task()
调用只调用一次回调,就可以保证 score 的最终值等于任意长度。
Javascript 的单线程特性保证了永远不会有两个 Javascript 片段同时运行。 相反,由于浏览器和 node.js 中 Javascript 的事件驱动性质,一段 JS 运行完成,然后从事件队列中提取下一个事件并触发回调,该回调也将运行完成。
没有中断驱动的 Javascript 之类的东西(其中一些回调可能会中断当前正在运行的其他一些 Javascript)。 一切都通过事件队列进行序列化。 这是一个巨大的简化,并防止了许多棘手的情况,否则当您有多个线程并发运行或中断驱动代码时,安全编程需要大量工作。
仍然存在一些需要关注的并发问题,但它们更多地与多个异步回调都可以访问的共享状态有关。 虽然在任何给定时间只有一个人会访问它,但包含多个异步操作的一段代码仍然有可能使某个状态处于“中间”状态,而它同时处于多个异步操作的中间一些其他异步操作可以运行并且可以尝试访问该数据的点。
您可以在此处阅读有关 Javascript 事件驱动性质的更多信息: JavaScript 如何在后台处理 AJAX 响应? 该答案还包含许多其他参考资料。
另一个类似的答案讨论了可能的共享数据竞争条件的类型: 此代码是否会导致套接字 io 中的竞争条件?
其他一些参考:
如何防止事件处理程序在 javascript 中一次处理多个事件?
具有多个并发请求的 Node.js 服务器,它是如何工作的?
为了让您了解 Javascript 中可能发生的并发问题(即使没有线程和中断,这是我自己的代码中的一个示例。
我有一个 Raspberry Pi node.js 服务器,用于控制我家中的阁楼风扇。 它每 10 秒检查两个温度探测器,一个在阁楼内,一个在屋外,并决定如何控制风扇(通过继电器)。 它还记录可以在图表中显示的温度数据。 每小时一次,它将内存中收集的最新温度数据保存到一些文件中,以便在断电或服务器崩溃时持久保存。 该保存操作涉及一系列异步文件写入。 这些异步写入中的每一个都将控制权交还给系统,然后在调用异步回调信号完成时继续。 因为这是一个低内存系统,并且数据可能会占用可用 RAM 的很大一部分,所以在写入之前不会将数据复制到内存中(这根本不实用)。 所以,我正在将实时内存数据写入磁盘。
在任何这些异步文件 I/O 操作期间的任何时候,在等待回调以表示所涉及的许多文件写入完成时,服务器中的一个计时器可能会触发,我会收集一组新的温度数据和这将尝试修改我正在编写的内存数据集。 这是一个等待发生的并发问题。 如果它在我写了一部分数据时更改了数据,并且在写其余部分之前等待该写完成,那么写入的数据很容易最终损坏,因为我将写出一部分数据,数据将从我的下方被修改,然后我将尝试写出更多数据而没有意识到它已被更改。 那是并发问题。
我实际上有一个console.log()
语句,它在我的服务器上发生此并发问题时明确记录(并且由我的代码安全处理)。 它在我的服务器上每隔几天发生一次。 我知道它就在那里,而且是真的。
有很多方法可以解决这些类型的并发问题。 最简单的方法是在内存中复制所有数据,然后写出副本。 因为没有线程或中断,所以在内存中进行复制可以避免并发(在复制中间不会屈服于异步操作以创建并发问题)。 但是,在这种情况下这是不切实际的。 所以,我实现了一个队列。 每当我开始写作时,我都会在管理数据的对象上设置一个标志。 然后,只要系统想要在设置该标志时添加或修改存储数据中的数据,这些更改就会进入队列。 设置该标志时不会触及实际数据。 当数据已安全写入磁盘时,该标志将被重置并处理排队的项目。 安全地避免了任何并发问题。
因此,这是您必须关注的并发问题的一个示例。 Javascript 的一个很好的简化假设是,一段 Javascript 将运行到完成,而不会有任何线程被中断,只要它不故意将控制权返回给系统。 这使得处理上述并发问题变得更加容易,因为除非您有意识地将控制权交还给系统,否则您的代码永远不会被中断。 这就是为什么我们在我们自己的 Javascript 中不需要互斥体和信号量以及其他类似的东西。 如果需要,我们可以像上面描述的那样使用简单的标志(只是一个常规的 Javascript 变量)。
在任何完全同步的 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,直到将控制权返回给系统,然后(并且仅在那时),它从事件队列中获取下一个事件并调用与该事件关联的回调。
这是幕后事件的顺序。
console.log("A")
是输出。console.log("C")
是输出。console.log("D")
是输出。console.log("B")
。setTimeout()
回调完成执行并且解释器再次检查事件队列以查看是否有任何其他准备好运行的事件。 Node 使用事件循环。 您可以将其视为队列。 所以我们可以假设,你的 for 循环放置了function() { score++; }
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);
});
}
函数的两个调用不能同时发生(b/c 节点是单线程的),所以不会有问题。 唯一的问题是如果在某些情况下 async_task(..) 丢弃回调。 但是,如果,例如,'async_task(..)' 只是用给定的函数调用 setTimeout(..),那么是的,每个调用都会执行,它们永远不会相互冲突,并且 'score' 将具有预期的值, 'arbitrary_length', 最后。
当然,'arbitrary_length' 不能大到耗尽内存或溢出任何持有这些回调的集合。 但是没有线程问题。
我确实认为对于其他人来说值得注意的是,您的代码中有一个常见的错误。 对于变量 i,在将其传递到 async_task() 之前,您需要使用 let 或重新分配给另一个变量。 当前的实现将导致每个函数获得 i 的最后一个值。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.