今天我花了很多时间试图找到我自己的问题的答案,这里有一些 cmets 和其他答案。我在这里分享我的发现,以防其他人认为它们有用。
浏览器 JavaScript 中的事件驱动设计
以这种方式设计 JavaScript 的决定似乎主要与 DOM Event Architecture 的要求有关。在本规范中,我们可以找到与事件顺序和事件循环的实现相关的explicit requirements。 HTML5 规范更进一步,明确定义术语并声明specific requirements for the event loop implementation。
这肯定推动了浏览器中 JavaScript 执行引擎的设计。在 Opera 发布的这篇文章Timing and Synchronization in JavaScript 中,我们可以清楚地看到,这些需求是 Opera 浏览器设计背后的驱动力。同样在来自 Mozilla 的另一篇名为 Concurrency Model and Event Loop 的文章中,我们可以找到与 Mozilla 实现的相同事件驱动设计概念的清晰解释(尽管该文档似乎已过时)。
使用事件循环来处理这类应用程序并不新鲜。
处理用户输入是交互中最复杂的方面
编程。应用程序可能对多个输入敏感
设备,例如鼠标和键盘,并且可以在这些设备之间多路复用
多个输入设备(例如不同的窗口)。管理这个
多对多映射通常属于用户界面
管理系统 (UIMS) 工具包。由于大多数 UIMS 都已实现
在顺序语言中,他们必须求助于各种技术
模拟必要的并发性。通常,此工具包使用
event-loop 监控输入事件流并将事件映射到由
应用程序员。
- Jonh H. Reppy - Concurrent Programming in ML
事件循环的使用出现在其他著名的 UI 工具包中,例如 Java Swing 和 Winforms。在 Java 中,所有 UI 工作都必须在 Winforms 中的 EventDispatchThread 内完成,而在 Winforms 中,所有 UI 工作都必须在创建 Window 对象的 within the thread 内完成。因此,即使这些语言支持真正的多线程,它们仍然要求所有 UI 代码在单个执行线程中运行。
Douglas Crockford 在这个名为 Loopage 的精彩视频中解释了 JavaScript 中事件循环的历史(值得一看)。
Node JavaScript 中的事件驱动设计
现在,为 Node.js 使用事件驱动设计的决定不那么明显了。 Crockford 在上面分享的视频中给出了很好的解释。而且,在书中The Past, Present and Future of JavaScript,其作者Axel Rauschmayer 说:
2009—Node.js,服务器上的 JavaScript。 Node.js 让你实现
在负载下表现良好的服务器。为此,它使用事件驱动
非阻塞 I/O 和 JavaScript(通过 V8)。 Node.js 的创建者 Ryan Dahl
提到选择 JavaScript 的原因如下:
- “因为它是裸露的,不附带 I/O API。” [Node.js 可以因此引入自己的非阻塞 API。]
- “Web 开发人员已经在使用它。” [JavaScript 是一种广为人知的语言,尤其是在网络环境中。]
- “DOM API 是基于事件的。每个人都已经习惯了在没有线程和事件循环的情况下运行。” [Web 开发人员不怕
回调。]
因此,看起来 Node.js 的创建者 Ryan Dahl 考虑了浏览器中 JavaScript 的当前设计,以决定哪个应该是他的 Node.js 非阻塞、事件驱动解决方案的实现。
Node.js 的最新实现seems to use 一个名为libuv 的库,专为此类应用程序的实现而设计。这个库是节点设计的核心部分。我们可以找到definition of event loops in its documentation。显然,这在 Node.js 的当前实现中起着重要作用。
关于其他 EcmaScript 兼容引擎
EcmaScript 规范没有提供关于如何在 JavaScript 中处理并发的要求。因此,这是由语言的实现决定的。可以轻松使用其他并发模型,而不会使实现与标准不兼容。
我发现的最好的两个例子是为 Oracle 为 JDK8 创建的新 Nashorn JavaScript Engine 和由 Mozilla 创建的 Rhino JavaScript Engine。它们都与 EcmaScript 兼容,并且都允许创建 Java 类。这些引擎中没有任何东西需要使用事件驱动编程来处理并发性。这些引擎可以访问 Java 类库,并且由于它们在 JVM 之上运行,因此它们可能可以访问该平台中提供的其他并发模型。
考虑以下取自JavaScript, The Definitive Guide 的示例来说明如何使用Rhino JavaScript。
print(x); // Global print function prints to the console
version(170); // Tell Rhino we want JS 1.7 language features
load(filename,...); // Load and execute one or more files of JavaScript code
readFile(file); // Read a text file and return its contents as a string
readUrl(url); // Read the textual contents of a URL and return as a string
spawn(f); // Run f() or load and execute file f in a new thread
runCommand(cmd, // Run a system command with zero or more command-line args
[args...]);
quit() // Make Rhino exit
您可以看到可以生成一个新线程以在独立的执行线程中运行 JavaScript 文件。
关于事件驱动设计、多核和真正的并发
我在这个主题上找到的最好的解释来自JavaScript The Definitive Guide这本书。在本书中,David Flanagan 解释说:
客户端 JavaScript 的基本特征之一是它
是单线程的:浏览器永远不会在
同时,它永远不会在事件处理程序运行时触发计时器
以跑步为例。并发更新应用程序状态或
该文档根本不可能,而客户端程序员可以
不需要思考,甚至不需要理解并发编程。一种
推论是客户端 JavaScript 函数也不能运行
long:否则它们会占用事件循环和网络浏览器
将对用户输入无响应。这就是阿贾克斯的原因
API 始终是异步的,这是客户端的原因
JavaScript 不能有简单的同步 load() 或 require()
用于加载 JavaScript 库的函数。
Web Workers 规范非常谨慎地放宽了
客户端 JavaScript 的单线程要求。工人”
它定义了有效的并行执行线程。网络工作者
然而,生活在一个独立的执行环境中,没有
访问 Window 或 Document 对象并可以与
主线程只通过异步消息传递。这意味着
DOM 的并发修改仍然是不可能的,但它
也意味着现在有一种方法可以使用同步 API 并编写
长时间运行的函数不会停止事件循环并挂起
浏览器。创建一个新的工人不是像重量级的操作
打开一个新的浏览器窗口,但工作人员不是轻量级线程
或者,创造新的工人来执行是没有意义的
琐碎的操作。复杂的 Web 应用程序可能会发现它对
创建数十个工作人员,但应用程序不太可能
成百上千的工人将是实用的。
Node.js 真正的并行性怎么样?
Node.js 是一种快速发展的技术,也许这就是为什么很难找到最新的意见的原因。但基本上,由于它遵循与浏览器相同的事件驱动模型,因此不可能简单地编写一段代码并期望它会利用我们在服务器中的多个内核。由于 Node.js 是使用非阻塞技术实现的,我们可以假设每次我们执行某种形式的 I/O(即读取文件、通过套接字发送内容、写入数据库等)时,都在后台,节点引擎可能会产生多个线程并可能利用内核,但我们的代码仍将串行运行。
这些天来,node.js clustering 似乎是解决此问题的方法。还有一些像 Node Worker 这样的库似乎在 node.js 中实现了 Web Worker 的概念。这些库基本上让我们在 node.js 中生成新的独立进程。 (虽然我还没有尝试过这个)。
便携性怎么样?
看起来,就并发模型而言,我们无法保证所有这些库在所有环境中都能正常运行。
虽然在浏览器领域它们似乎都工作得差不多,而且由于 Node.js 在事件循环中运行,许多事情可能仍然有效,但不能保证这在其他引擎中也能正常工作。我想这可能是 EcmaScript 与其他更广泛的规范(如定义 Java 虚拟机或 CLR)相比的缺点之一。
也许稍后会标准化。在 EcmaScript 的未来,今天正在讨论更多的并发思想。见EcmaSript Wiki: Strawman Proposals Communicating Event-Loop Concurrency and Distribution