问题 JavaScript执行引擎未指定?


我最近开始学习JavaScript。我一直在创建应用程序 Node.js的 和  几个月了

令我困惑的一个主要方面是如何在JavaScript中编写异步代码,我不必担心像 线程同步竞争条件

所以,我找到了几篇有趣的文章([1][2])解释了如何保证我写的任何代码片段当时都会由一个线程执行。最重要的是,我的所有异步代码都被安排在一个内部的某个时刻执行 事件循环。这听起来很像OS调度程序可以在具有单个处理器的机器中工作,其中每个进程都安排在有限的时间内使用处理器,给我们假的并行感。而回调就像 中断

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

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

所以,我有一套我认为有趣的问题。我很感激任何答案,提供见解,评论或简单的参考,指出我正确的方向,以了解以下内容:

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

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


2539
2017-11-22 13:55


起源

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


答案:


您做出的某些假设/弱参考会引导您得出这个结论。他们之中有一些是:

  1. ECMAScript ECMA-XXX vs JavaScript与JavaScriptEngine:

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

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

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

    如果您使用node.JS,则可能使用了核心库fs / net / http。这些使用与libuv提供的事件循环挂钩的事件发射器。这是JavaScriptEngine的扩展 V8,形成 Node.js的 平台。这里的事件循环涉及线程,套接字,文件或抽象请求等对象。但事件并非源于此。它首先在浏览器中使用。浏览器实现了一个DOM,它需要使用事件来处理HTML元素。查看DOM 规范 和一个实现 Mozilla的。他们使用事件并要求 事件循环 构建在JSEngine之上,供浏览器使用。  添加DOM接口 V8 引擎嵌入。

    是的,您会觉得这很常见,因为所有浏览器都有必要的DOM API。节点开发人员在libuv的帮助下将这种新颖的事务处理提交到服务器,libuv为服务器上所需的低级操作提供非阻塞,异步抽象。如前所述,并非所有服务器框架都使用事件循环。以Rhino为例,它实际上使用Java Classes作为文件,套接字(一切)。如果您实际使用核心Java IO,则文件操作是同步的。

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

  • 在上面第2点解释

  • 是的你可以。看看Rhino,还有很多其他的。它可能在节点中,但节点适合作为高性能的Web服务器,并且可能违背其禅宗。

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

  • 这因发动机而异。它是如何嵌入的。浏览器将具有DOM并因此具有事件循环。服务器可以变化。检查他们的规格。

  • 对于浏览器,可以在很大程度上使它们在它们之间移植。对服务器没有承诺。


6
2017-11-22 21:12



这是一个很好的答案,有很多有趣的见解和有用的信息。在这里,我的投票+1。 - Edwin Dalorzo


  1. 事件循环与javascript本身没有任何关系,它是环境的一部分,而不是js引擎。由于javascript主要是为了操纵用户界面而设计的,因此它在事件循环中被大量使用。但事件循环是UI实现的一部分,不仅仅是在javascript中,而是在任何语言中。

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

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

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


4
2017-11-22 14:12



嗯,问题是执行环境的设计是代码可移植性的基础。如果您不能假设有关执行环境的一些基本前提,您如何编写适用于节点和浏览器的库?听起来确实应该指明,你不觉得吗? - Edwin Dalorzo
请注意JavaScript!= ECMAScript - el.pescado
@ el.pescado,因此ECMAScript是构成JavaScript基础的脚本语言。因此,我们可以放心地假设JavaScript执行EcmaScript所做的任何事情。只要它不偏离标准,它就可以做得更多。但不知道如何知道这有助于这种情况。 - Edwin Dalorzo
@ el.pescado不是!但区别很浅。 - vkurchatkin
@EdwinDalorzo我只是说它不是语言水平的东西。例如,C也没有定义任何并发原语(至少在C11之前)。因此,您可以使用基于事件或基于线程的并发。像Java这样的语言有很大的标准库,但这只是另一种方法,它具有很高的维护成本。 ES规范紧凑且专注。事件循环相关的东西只是web apis的一个实现细节。如果您想了解它是如何实现的,请查看节点libuv。它提供了事件循环的c实现,并且实际上与节点的其他部分分离。 - vkurchatkin


今天我花了很多时间试图找到我自己问题的答案,并在这里给我留下一些评论和其他答案。我在这里分享我的发现,以防其他人认为它们有用。

用于浏览器的JavaScript事件驱动设计

以这种方式设计JavaScript的决定似乎主要与the的要求有关 DOM事件架构。在本说明书中我们可以找到 明确的要求 与事件顺序和事件循环的实现有关。 HTML5规范更进一步,明确定义术语和状态 事件循环实现的特定要求

这肯定会推动浏览器中JavaScript执行引擎的设计。在这篇文章中 JavaScript中的计时和同步 由Opera发布我们可以清楚地看到这些要求是Opera浏览器设计背后的驱动力。另外在另一篇来自Mozilla的文章中,命名 并发模型和事件循环,我们可以找到Mozilla实现的相同事件驱动设计概念的清晰解释(虽然该文档似乎已经过时)。

使用事件循环来处理这种应用程序并不新鲜。

处理用户输入是交互式最复杂的方面   节目。应用程序可能对多个输入敏感   设备,如鼠标和键盘,可以在这些设备之间进行多路复用   多个输入设备(例如,不同的窗口)。管理这个   多对多映射通常在省内 用户界面   管理系统 (UIMS)工具包。由于大多数UIMS都已实施   在顺序语言中,他们必须采用各种技术   模仿必要的并发性。通常,此工具包使用    事件循环 监视输入事件流并将事件映射到 回电话 功能(或 事件处理程序)由...提供   应用程序员。    - Jonh H. Reppy - ML中的并发编程

事件循环的使用存在于其他着名的UI工具包中,如Java Swing和Winforms。在Java中,所有UI工作都必须在 EventDispatchThread Winforms中的所有UI工作必须完成 在线程内 创建了Window对象。因此,即使这些语言支持真正的多线程,它们仍然需要在单个执行线程中运行所有UI代码。

Douglas Crockford在这个名为的视频中解释了JavaScript中事件循环的历史 Loopage (值得一看)。

用于节点的JavaScript中的事件驱动设计

现在,为Node.js使用事件驱动设计的决定有点不太明显。 Crockford在上面分享的视频中给出了一个很好的解释。但是,在书中, 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是一种广为人知的语言,尤其是在Web环境中。
  • “DOM API是基于事件的。每个人都习惯于在没有线程和事件循环的情况下运行。“[Web开发人员并不害怕   回调。]

因此,看起来像Node.js的创建者Ryan Dahl考虑了浏览器中当前的JavaScript设计,以决定哪些应该是针对Node.js的非阻塞,事件驱动的解决方案的实现。

Node.js的最新实现 好像用了 一个叫做的图书馆 libuv,专为实现此类应用程序而设计。该库是节点设计的核心部分。我们可以找到 在其文档中定义事件循环。显然,这在Node.js的当前实现中起着重要作用。

关于其他EcmaScript兼容引擎

EcmaScript规范没有提供有关如何在JavaScript中处理并发性的要求。因此,这是由语言的实现决定的。可以轻松使用其他并发模型,而不会使实现与标准不兼容。

我找到的最好的两个例子是新的 Nashorn JavaScript引擎 为Oracle创建的JDK8和 Rhino JavaScript引擎 由Mozilla创建。它们都兼容EcmaScript,它们都允许创建Java类。这些引擎中的任何内容都不需要使用事件驱动的编程来处理并发。这些引擎可以访问Java类库,因为它们运行在JVM之上,所以它们可以访问此平台中提供的其他并发模型。

考虑以下示例来自 JavaScript,权威指南 说明如何使用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权威指南。在这本书中,David Flanagan解释说:

客户端JavaScript的基本功能之一就是它   是单线程的:浏览器永远不会运行两个事件处理程序   同时,它在事件处理程序时永远不会触发计时器   例如,跑步。应用程序状态或的并发更新   文档根本不可能,客户端程序员也可以   不需要考虑甚至理解并发编程。一个   必然结果是客户端JavaScript函数也不能运行   long:否则他们将占用事件循环和Web浏览器   将对用户输入无响应。这就是Ajax的原因   API始终是异步的,也是客户端的原因   JavaScript不能简单,同步 load() 要么 require()   用于加载JavaScript库的函数。

Web Workers规范非常谨慎地放松了   客户端JavaScript的单线程要求。工人”   它定义的是有效并行的执行线程。网络工作者   然而,生活在一个独立的执行环境中,没有   访问Window或Document对象并可以与之通信   主线程只能通过异步消息传递。这意味着   DOM的并发修改仍然不可能,但它   也意味着现在有一种使用同步API和写入的方法   长时间运行的函数,不会停止事件循环并挂起   浏览器。创建一个新的工作者不是一个重量级的操作   打开一个新的浏览器窗口,但工人不是轻量级线程   或者,创建新的工作人员没有意义   琐碎的行动。复杂的Web应用程序可能会发现它很有用   创造了数十名工人,但申请人不太可能   数百或数千名工人将是实际的。

那么Node.js真正的并行性呢?

Node.js是一种快速发展的技术,也许这就是为什么很难找到最新的观点。但基本上,由于它遵循与浏览器相同的事件驱动模型,因此不可能简单地编写一段代码并期望它将利用我们在服务器中的多个核心。由于Node.js是使用非阻塞技术实现的,我们可以假设每次我们做某种形式的I / O(即读取文件,通过套接字发送内容,写入数据库等),引擎盖下,节点引擎可能会产生多个线程并可能利用核心,但我们的代码仍然可以串行运行。

这些天,它看起来像 node.js聚类 是这个问题的解决方案。还有一些类似的库 节点工作者 似乎在节点中实现Web Worker概念。这些库基本上让我们在node.js中生成新的独立进程。 (虽然我还没有尝试过这个)。

便携性怎么样?

看起来,就并发模型而言,我们无法保证所有这些库在所有环境中都能发挥出色。

虽然在浏览器领域它们似乎都是类似的,并且由于Node.js在事件循环中运行,许多事情可能仍然有效,但不保证这应该在其他引擎中工作。我想这可能是EcmaScript与其他更广泛的规范(如定义Java虚拟机或CLR​​)相比的缺点之一。

也许某些事情会在以后标准化。在EcmaScript的未来,今天正在讨论更多的并发思想。见 EcmaSript Wiki:稻草人提案传达事件循环并发和分发


2