我是否保证 score 的最终值等于
任意长度?
是的,只要所有async_task()调用调用回调一次且只调用一次,就可以保证score的最终值等于任意长度。
正是 Javascript 的单线程特性保证了永远不会有两段 Javascript 在同一时间运行。相反,由于浏览器和 node.js 中 Javascript 的事件驱动特性,一个 JS 运行到完成,然后从事件队列中拉出下一个事件并触发一个回调,该回调也将运行到完成。
不存在中断驱动的 Javascript(其中一些回调可能会中断当前正在运行的其他一些 Javascript)。一切都通过事件队列进行序列化。这是一个巨大的简化,并且可以防止很多棘手的情况,否则当您有多个线程同时运行或中断驱动的代码时,这些情况将需要大量工作才能安全地编程。
还有一些并发问题需要关注,但更多的是与多个异步回调都可以访问的共享状态有关。虽然在任何给定时间只有一个人会访问它,但一段包含多个异步操作的代码仍然有可能使某些状态处于“中间”状态,而它正处于多个异步操作的中间。一些其他异步操作可以运行并可能尝试访问该数据的点。
您可以在此处阅读有关 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 作为“事件驱动”语言的含义。作为一个例子,如果你有这个代码:
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,直到将控制权返回给系统,然后(并且仅在那时)从事件队列中获取下一个事件并调用与该事件关联的回调。
这是幕后事件的顺序。
- 此 JS 开始运行。
-
console.log("A") 是输出。
- 从现在开始的 500 毫秒内安排了一个计时器事件。计时器子系统使用本机代码。
-
console.log("C") 是输出。
- 代码进入自旋循环。
- 在自旋循环的中途某个时间点,之前设置的计时器已准备好触发。具体如何工作由解释器实现决定,但最终结果是将计时器事件插入到 Javascript 事件队列中。
- 自旋循环结束。
-
console.log("D") 是输出。
- 这段 Javascript 完成并将控制权返回给系统。
- Javascript 解释器看到当前的 Javascript 已完成,因此它检查事件队列以查看是否有任何未决事件等待运行。它找到计时器事件和与该事件关联的回调并调用该回调(开始一个新的 JS 执行块)。该代码开始运行并输出
console.log("B")。
-
setTimeout() 回调完成执行,解释器再次检查事件队列以查看是否有任何其他事件可以运行。