【问题标题】:JavaScript Execution Engine Unspecified?JavaScript 执行引擎未指定?
【发布时间】:2013-12-07 10:25:56
【问题描述】:

我最近开始学习 JavaScript。几个月来,我一直在使用 Node.jsAngular 创建应用程序。

让我感到困惑的主要方面之一是如何在 JavaScript 中编写异步代码,而我不必担心thread synchronizationrace conditions 等事情。

所以,我发现了几篇有趣的文章([1],[2]),这些文章解释了我如何保证我编写的任何代码在当时始终由单个线程执行。最重要的是,我所有的异步代码都只是安排在event loop 内的某个时间点执行。这听起来很像操作系统调度程序将在具有单个处理器的机器上工作,其中每个进程都被安排在有限的时间内使用处理器,给我们一种虚假的并行感。回调就像interrupts

这些文章没有提供任何特定的参考资料,所以我认为关于 JavaScript 执行引擎如何工作的最佳来源当然应该是语言规范,所以我给了我最新的 EcmaScript 5.1 副本。

令我惊讶的是,我发现这里没有指定这种执行行为。怎么会?这看起来像是在浏览器和节点中的所有 JavaScript 执行引擎中完成的基本设计选择。有趣的是,我无法找到为任何特定引擎指定的地方。事实上,我不知道人们是如何发现这就是事情的运作方式,以至于在上面引用的书籍和博客中如此明确地肯定了这一点。

所以,我有一组我认为有趣的问题。我将不胜感激任何提供见解、评论或简单参考的答案,这些答案为我指明了正确的方向以了解以下内容:

  • 由于 EcmaScript 没有指定 JavaScript 执行引擎应该与事件循环一起工作,为什么 JavaScript 的实现看起来会以这种方式工作,不仅在浏览器中,而且在 Node.js 中?
  • 这是否意味着我可以实现一个新的与 EcmaScript 兼容的 JavaScript 引擎,它实际上提供了真正的多线程功能以及同步锁、条件等特性?
  • 如果我想执行密集的 CPU 密集型任务,这种使用事件循环的执行模型是否会阻止我利用多核?我的意思是,我当然可以将任务分成几块(如其中一篇文章中所述),但这仍然是串行执行的,而不是并行执行的。那么,JavaScript 引擎如何利用多核来运行我的代码呢?
  • 您是否知道任何其他有信誉的来源,其中正式指定了任何特定 JavaScript 引擎实现的这种行为?
  • 如果我们不能对执行环境做出一些假设,代码如何在库和引擎之间移植?

看起来问题太多了,也许这篇文章太宽泛而无法回答。如果它被关闭,我会尝试在不同的线程中询问他们。但它们都围绕着这样一个事实,即我想更好地理解为什么 JavScript 和 Node 被设计为带有事件循环,如果在某个地方(除了浏览器源代码之外)指定了它,我可以阅读并更深入地了解设计和决策带到这里,更重要的是,确切地知道人们写书和发帖的信息来源是什么。

【问题讨论】:

  • Rhino 中的 JavaScript 或 Java ScriptManager 框架并不真正适合事件处理运行时模型。
  • 什么是“JavaScript 执行引擎”?根据平台的不同,它会有所不同。 NodeJS 有一个与网络浏览器非常不同的。
  • 而且编程语言规范通常不会指定如何在给定平台上执行/实现。
  • @WiredPrairie 也许我搞砸了术语。我来自 Java 背景。在 Java 中,JVM 是完全指定的。任何愿意实现虚拟机的人都可以简单地遵循规范。现在,在 EcmaScript 的情况下,我可以按照规范来实现该语言,但它似乎缺乏关于 EcmaScript 引擎/评估器应该如何处理诸如并发之类的事情的重要细节。
  • @WiredPrairie 我明白了。但是,如果我今天读了一本关于基于浏览器的 JavaScript 的书,它们都假设 JavaScript 在事件循环上运行,无论其实现如何。因此,所有主流浏览器都遵循该标准。所以,如果我想写一本关于 JavaScript 的书,我怎么知道所有这些浏览器都在事件循环中运行它们的 JavaScript?我根本找不到官方解释的地方。那么人们怎么知道这是它的工作方式呢?例如,SpiderMonkey 引擎文档几乎没有详细说明线程安全。我在 MDN 上都找不到这个。

标签: javascript multithreading node.js asynchronous ecmascript-5


【解决方案1】:

今天我花了很多时间试图找到我自己的问题的答案,这里有一些 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

【讨论】:

    【解决方案2】:

    您所做的某些假设/薄弱的参考会导致您得出这个结论。其中一些是:

    1. ECMAScript ECMA-XXX vs JavaScript vs JavaScriptEngine:

      ECMAscript 是一种语言规范,由 ECMA International 提供。 JavaScript 是符合 ECMAscript 的最广泛使用的 Web 语言。在大多数情况下,ECMAScript 和 JavaScript 是同义词(记住有 ActionScript)。 JavaScriptEngine 是 JavaScript 语言代码的实现(解释器)。它是一个从头开始工作的有血有肉的程序,不像 ECMAScript 只描述 JavaScript 的最终目标和行为,而 JavaScript 是使用 ECMAScript 标准的代码。您会发现引擎不仅仅符合 ECMAScript 标准。它们位于规范/实施范围的末端。这方面的例子是 ECMA-262/JavaScript/V8。

    2. 浏览器中的事件循环与 node.JS 中的事件循环(JSEngine 与 JSEnvironment):

      这看起来像是在浏览器和节点中的所有 JavaScript 执行引擎中完成的基本设计选择。

      如果您使用的是 node.JS,您可能使用过核心库 fs/net/http。这些使用与 libuv 提供的事件循环挂钩的事件发射器。这是对 JavaScriptEngine V8 的扩展,形成 node.JS 平台。这里的事件循环涉及线程、套接字、文件或抽象请求等对象。但事件并非起源于此。它最初用于浏览器。浏览器实现了一个 DOM,它需要事件来处理 HTML 元素。请参阅 DOM specification 和为 Mozilla 实现的一个。他们使用事件并且需要在 JSEngine 之上构建的 event loop 以供浏览器使用。 Chrome 将 DOM 接口添加到其嵌入的 V8 引擎中。

      是的,您会觉得这很常见,因为所有浏览器都需要 DOM API。 Node 开发人员借助 libuv 将这种新颖的事件处理技术引入服务器,它为服务器上所需的低级操作提供非阻塞、异步抽象。如前所述,并非所有服务器框架都使用事件循环。以 Rhino 为例,它从字面上使用 Java 类来处理文件、套接字(一切)。如果你实际使用核心 Java IO,文件操作是同步的。

    现在按顺序回答您的问题:

    • 在上述第 2 点中进行了解释

    • 是的,你可以。看看Rhino,还有很多其他的。在 node 中可能是可能的,但 node 是面向高性能网络服务器的,这可能违背了它的 zen。

    • 就像我说的事件循环位于 JSEngine 上。这是一种设计模式,最适合 IO。多线程设计在高 CPU 负载下效果更好。如果您想在 node.JS 中使用多个内核,请查看 cluster 模块。对于有网络工作者的浏览器

    • 这因发动机而异。以及它是如何嵌入的。浏览器将具有 DOM,因此具有事件循环。服务器可能会有所不同。检查他们的规格。

    • 对于浏览器,可以在它们之间很好地移植。没有对服务器的承诺。

    【讨论】:

    • 这是一个很好的答案,包含许多有趣的见解和有用的信息。在这里,让我投票 +1。
    【解决方案3】:
    1. 事件循环与 javascript 本身没有任何关系,它是环境的一部分,而不是 js 引擎。由于 javascript 主要是为操作用户界面而设计的,因此它在事件循环中被大量使用。但事件循环是 UI 实现的一部分,不仅在 javascript 中,而且在任何语言中。

    2. 是的,你可以。但它不仅仅是引擎,更像是环境/平台。我认为(但不太确定)你可以在 Rhino 中使用线程和相关的东西。

    3. 是的,确实如此。在节点中,这通常通过生成更多进程来解决,而在浏览器中,您可以使用 WebWorkers。

    4. 我无法想象比规范更好的来源。如果某些东西不存在,那么它就不是 javascript(又名 EcmaScript)的一部分

    【讨论】:

    • 嗯,问题是执行环境的设计是代码可移植性的基础。如果你不能假设一些关于你的执行环境的基本前提,你怎么能编写同时适用于节点和浏览器的库?听起来确实应该指定 in,你不觉得吗?
    • 请注意 JavaScript != ECMAScript
    • @el.pescado,所以 ECMAScript 是构成 JavaScript 基础的脚本语言。因此,我们可以放心地假设 JavaScript 完成了 EcmaScript 所做的任何事情。只要不偏离标准,它就可以做得更多。不过,不确定在这种情况下知道这一点有什么帮助。
    • @el.pescado 不是!但区别很浅。
    • @EdwinDalorzo 我只是说这不是语言级别的事情。例如,C 也没有定义任何并发原语(至少在 C11 之前)。所以你可以使用基于事件或基于线程的并发。像 Java 这样的一些语言拥有庞大的标准库,但这只是另一种方法,维护成本很高。 ES 规范紧凑且专注。事件循环相关的东西只是 web api 的一个实现细节。如果您想了解它是如何实现的,请查看nodes libuv。它提供事件循环的 c 实现,并且真正与节点的其他部分解耦。
    猜你喜欢
    • 1970-01-01
    • 2020-07-19
    • 2011-12-20
    • 1970-01-01
    • 2017-05-28
    • 1970-01-01
    • 1970-01-01
    • 2011-01-04
    • 2011-09-29
    相关资源
    最近更新 更多