探索 Node.js 中事件处理循环背后的理论
P粉564192131
P粉564192131 2024-04-01 19:35:24
0
1
513

我正在查看这个描述 JavaScript 中的文件遍历算法的要点

// ES6 version using asynchronous iterators, compatible with node v10.0+

const fs = require("fs");
const path = require("path");

async function* walk(dir) {
    for await (const d of await fs.promises.opendir(dir)) {
        const entry = path.join(dir, d.name);
        if (d.isDirectory()) yield* walk(entry);
        else if (d.isFile()) yield entry;
    }
}

// Then, use it with a simple async for loop
async function main() {
    for await (const p of walk('/tmp/'))
        console.log(p)
}

我被(语言特性的,而不是作者的)将 async/await 塞进算法的每个缝隙的冲动所震惊。我不太了解 Node.js 的架构,但我认为严重异步特性背后有某种意图?对于主要是声明性过程 C/C 线程/进程模型的我来说,这非常令人困惑和新鲜。我想知道的不是“模型是什么”或“更好吗”,正如人们可能有的意见,而是“驱动异步性的想法是什么?”这是浏览器对响应能力的需求超过基于通用任务的效率的遗留问题吗?

我的问题是“为什么有这么多异步性?”。我不是在寻求意见,而是在寻找了解 Node.js 或 JavaScript 的历史和演变的人来解释其架构。

要点:https://gist.github.com/lovasoa/8691344

P粉564192131
P粉564192131

全部回复(1)
P粉328911308

嗯,在高层次上,该句子的第一部分和该句子的第二部分是冲突的。如果服务器效率低下,那么它将无法正确响应一堆同时到达的客户端请求。因此,您希望提高服务器的效率,以便它能够尽可能响应客户端请求。


现在,我们需要回顾几个步骤并了解您所显示的代码中到底发生了什么。首先,虽然该语言恰好是 Javascript,但该代码的核心逻辑以及它如何使用 asyncawait 和生成器不仅仅是因为 Javascript 这种语言。这是因为Javascript运行的特定环境,在本例中是nodejs。

该环境使用事件循环并运行单个 Javascript 线程。其他操作系统线程用于各种系统事物和一些库实现,但是当nodejs运行您的Javascript时,它一次仅运行一段Javascript(单线程)。

同时,当您设计服务器时,您希望它能够响应大量传入请求。您不希望它必须处理一个请求并让所有其他请求等到第一个请求完成后才开始处理下一个请求。但是,nodejs 事件循环模型不使用多个线程,因此不会同时直接运行多个请求处理程序。

nodejs 部署的解决方案来自这样一个事实:对于各种各样的服务器请求处理程序来说,主要活动以及花费大部分时间来处理请求的是 I/O(例如网络、文件 I/O 或数据库) /O)。这些是较低级别的操作,有自己的(非 Javascript)实现。

因此,它为所有 I/O 操作部署异步模型。一个编写良好的服务器可以启动异步 I/O 操作,当它处理时(不是在 Nodejs 解释器本身运行的代码中),解释器和 NodeJS 事件循环可以自由地做其他事情,处理另一个请求。一段时间后,当该异步操作完成时,一个事件将被插入到事件循环中,当解释器完成正在执行的任何操作时,它就可以处理该异步操作的结果并继续该操作。

通过这种方式,Javascript 仅在单个线程中执行,但许多传入请求可以同时“处理中”。

是的,这是一个与老式 C/C 线程模型完全不同的模型。您要么学习这种不同的模型,以便在 Nodejs 中编写高效且有效的服务器代码,要么不学习。如果您想坚持使用旧模型,那么选择一个不同的环境,在线程中运行请求处理程序(Java、C 等),并且旨在很好地做到这一点(当然,还有相关的设计和测试开销)正确编写并彻底测试所有多线程并发性)。

nodejs 模型的一大好处是它不易受到多线程执行模型所存在的许多并发问题的影响。 Nodejs 模型也有一些缺点,偶尔需要解决方法。例如,如果您在用 Javascript 编写的请求处理程序中包含大量占用 CPU 的代码,那么这仍然会使事情陷入困境,并且需要找到一种方法将占用大量 CPU 的代码从主事件循环中转移到其他线程中,或者进程(甚至可能是工作队列)。但是,I/O 都是异步的,并且可以保留在主线程中,而不会造成任何问题。

需要在单独的并发线程中的代码越少,您可能遇到的并发错误就越少,并且代码更容易完全测试。

嗯,你想要一个循环,因为你试图循环某些东西。当您不想阻止事件循环或者这是您必须完成任务的唯一操作类型(例如在数据库中进行查找)时,您希望在该循环中使用异步操作。

使用异步操作并不是为了优化某些东西。这是关于编写良好的服务器代码并且不阻塞事件循环的核心设计。而且,事实上,nodejs 中的可能接口(例如数据库接口或网络接口)仅提供异步接口。

您提出这个问题的方式表明,您将受益于更好地理解核心 Nodejs 架构并阅读更多有关事件循环如何工作以及异步 I/O 操作如何工作的信息。

首先,如果您使用的是异步 API(例如网络或数据库),那么您别无选择。您将进行异步代码设计来使用该 API。如果您可以选择使用异步还是同步 API(就像在 Node.js 中使用文件系统访问一样),那么您可以选择是否在每个 API 调用上阻止事件循环或不阻止事件循环。如果您阻止事件循环,您将严重损害服务器的可扩展性和响应能力。


该特定代码示例确实尝试在与 asyncawait、生成器和 yield 相同的实现中使用异步语言功能的厨房水槽。我一般不这样做。该实现的重点是能够创建一个可以非常简单地使用的接口,如下所示:

for await (const p of walk('/tmp/')) {
    ...
}

walk() 的内部是异步的。此实现向 API 用户隐藏了几乎所有异步实现的复杂性,这使得 API 更易于编码。通过将单个 await 放在正确的位置,API 的用户几乎可以像同步一样进行编码。这些 Javascript 语言功能(promises、async、await、generators 等)的目的是使异步操作更易于编码。

事件循环模型的优点

编程简单。从线程访问共享数据时通常不必处理并发问题,因为所有 Javascript 都在同一线程中运行,因此所有共享数据访问都来自同一线程。您不需要互斥体来访问共享数据。您不会因这些互斥体而面临任何死锁的风险。

错误更少。从线程访问公共数据要编写无错误的代码要困难得多。如果编写得不完美,代码可能会受到竞争条件的影响或缺少并发保护。而且,这些竞争条件通常很难测试,并且可能直到您的服务器处于高负载时才会显现出来,即使这样也不容易重现。

更高的可扩展性(在某些情况下)。对于主要受 I/O 限制的代码,协作事件循环模型可能会带来更高的可扩展性。这是因为处理中的每个请求不会导致单独的操作系统线程及其增加的开销。相反,只有少量的应用程序状态,通常位于与下一个回调或承诺等待相关的闭包中。

有关事件循环编程的文章

为什么酷孩子使用事件循环 -这恰好是关于在 Java 编程中使用事件循环,但讨论适用于任何环境

线程与事件的案例

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板