ChatGPT解决这个技术问题 Extra ChatGPT

当 Node.js 在内部仍然依赖于 Threads 时,它如何天生就更快?

我刚刚观看了以下视频:Introduction to Node.js,但仍然不明白您是如何获得速度优势的。

主要是,Ryan Dahl(Node.js 的创建者)曾说过 Node.js 是基于事件循环的,而不是基于线程的。线程很昂贵,只能留给并发编程专家使用。

随后,他展示了 Node.js 的架构堆栈,该堆栈具有底层 C 实现,内部有自己的线程池。所以很明显,Node.js 开发人员永远不会启动他们自己的线程或直接使用线程池......他们使用异步回调。我就这么理解。

我不明白的是 Node.js 仍然在使用线程......它只是隐藏了实现,所以如果 50 个人请求 50 个文件(当前不在内存中),那么这如何更快,那么不需要 50 个线程?

唯一的区别是,由于它是在内部管理的,Node.js 开发人员不必编写线程细节,但在其下面仍然使用线程来处理 IO(阻塞)文件请求。

那么,您不是真的只是解决一个问题(线程)并在该问题仍然存在时隐藏它:主要是多线程、上下文切换、死锁……等等?

一定有一些细节我还是不明白。

我倾向于同意你的说法,即这种说法有些过于简单化了。我认为node的性能优势归结为两点:1)实际线程都包含在相当低的级别,因此在大小和数量上仍然受到限制,从而简化了线程同步; 2) 通过 select() 的操作系统级“切换”比线程上下文交换更快。

j
jrtipton

实际上这里有一些不同的东西被混为一谈。但它始于线程真的很难的模因。因此,如果它们很难,您更有可能在使用线程时 1) 因错误而中断和 2) 尽可能高效地使用它们。 (2) 是你要问的那个。

想想他给出的一个例子,一个请求进来,你运行一些查询,然后对结果做一些事情。如果您以标准程序方式编写它,代码可能如下所示:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

如果传入的请求导致您创建一个运行上述代码的新线程,那么您将有一个线程坐在那里,在 query() 运行时什么都不做。 (根据 Ryan 的说法,Apache 使用单个线程来满足原始请求,而 nginx 在他所说的情况下优于它,因为它不是。)

现在,如果你真的很聪明,你会以一种环境可以在运行查询时执行其他操作的方式来表达上面的代码:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

这基本上就是 node.js 正在做的事情。你基本上是在装饰——由于语言和环境的原因,以一种方便的方式,因此关于闭包的要点——你的代码以这样一种方式,即环境可以聪明地知道什么运行,什么时候运行。这样一来,node.js 并不新鲜,因为它发明了异步 I/O(并不是任何人声称有这样的东西),但它的新鲜之处在于它的表达方式有点不同。

注意:当我说环境可以聪明地知道什么时候运行时,特别是我的意思是它用来启动一些 I/O 的线程现在可以用来处理一些其他请求,或者一些可以完成的计算并行,或启动其他一些并行 I/O。 (我不确定节点是否足够复杂,可以为同一个请求启动更多工作,但你明白了。)


好的,我绝对可以看到这如何提高性能,因为在我看来,你可以最大限度地利用你的 CPU,因为没有任何线程或执行堆栈只是在等待 IO 返回,所以 Ryan 所做的事情被有效地发现了一种缩小所有差距的方法。
是的,我要说的一件事是,他并没有找到缩小差距的方法:这不是一种新模式。不同的是,他使用 Javascript 让程序员以一种更方便这种异步的方式来表达他们的程序。可能是一个挑剔的细节,但仍然......
还值得指出的是,对于许多 I/O 任务,Node 使用任何可用的内核级异步 I/O api(epoll、kqueue、/dev/poll 等)
我仍然不确定我是否完全理解它。如果我们认为在 Web 请求中 IO 操作是处理请求所需的大部分时间,并且如果为每个 IO 操作创建一个新线程,那么对于 50 个快速连续的请求,我们将可能有 50 个线程并行运行并执行它们的 IO 部分。与标准 Web 服务器的不同之处在于,整个请求在线程上执行,而在 node.js 中只是其 IO 部分,但这是占用大部分时间并使线程等待的部分。
@SystemParadox 感谢您指出这一点。实际上,我最近对该主题进行了一些研究,实际上,当在内核级别正确实现异步 I/O 时,在执行异步 I/O 操作时不使用线程。相反,一旦 I/O 操作开始,调用线程就会被释放,当 I/O 操作完成并且有线程可用时,会执行回调。因此,如果正确实现了对 I/O 操作的异步支持,node.js 可以(几乎)使用一个线程并行运行 50 个并发请求和 50 个 I/O 操作。
n
nalply

笔记!这是一个古老的答案。虽然在粗略的轮廓上仍然如此,但由于 Node 在过去几年中的快速发展,一些细节可能已经发生了变化。

它使用线程是因为:

open() 的 O_NONBLOCK 选项不适用于文件。有些第三方库不提供非阻塞 IO。

要伪造非阻塞 IO,线程是必需的:在单独的线程中进行阻塞 IO。这是一个丑陋的解决方案,并导致大量开销。

在硬件层面上更糟:

使用 DMA,CPU 异步卸载 IO。

数据直接在 IO 设备和内存之间传输。

内核将其包装在一个同步的、阻塞的系统调用中。

Node.js 将阻塞系统调用包装在一个线程中。

这简直是愚蠢和低效的。但它至少有效!我们可以享受 Node.js,因为它隐藏了事件驱动的异步架构背后的丑陋和繁琐的细节。

也许将来有人会为文件实现 O_NONBLOCK ?...

编辑: 我和一位朋友讨论过这个问题,他告诉我,线程的替代方法是使用 select 轮询:指定超时 0 并在返回的文件描述符上执行 IO(现在它们得到保证不要阻止)。


窗户呢?
对不起,不知道。我只知道 libuv 是做异步工作的平台中立层。在 Node 开始时没有 libuv。然后决定分离 libuv,这使得特定于平台的代码更容易。换句话说,Windows 有它自己的异步故事,这可能与 Linux 完全不同,但对我们来说这并不重要,因为 libuv 为我们做了艰苦的工作。
T
Toby Eggitt

我担心我在这里“做错事”,如果是这样,请删除我并道歉。特别是,我看不到我是如何创建一些人创建的整洁的小注释的。但是,我对这个线程有很多关注/观察。

1)流行答案之一中伪代码中的注释元素

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

本质上是假的。如果线程正在计算,那么它不是在转动拇指,而是在做必要的工作。另一方面,如果它只是在等待 IO 的完成,那么它并没有使用 CPU 时间,内核中线程控制基础设施的全部意义在于 CPU 会找到一些有用的事情去做。此处建议的“摆弄你的拇指”的唯一方法是创建一个轮询循环,没有人编写过真正的网络服务器的代码不足以做到这一点。

2)“线程很难”,仅在数据共享的上下文中才有意义。如果您有本质上独立的线程,例如处理独立 Web 请求时的情况,那么线程非常简单,您只需编写如何处理一项作业的线性流程,并且知道它将处理多个请求,并且每个将有效地独立。就个人而言,我敢冒险,对于大多数程序员来说,学习闭包/回调机制比简单地编写从上到下的线程版本更复杂。 (但是,是的,如果你必须在线程之间进行通信,生活会变得非常艰难,但我不相信闭包/回调机制真的改变了这一点,它只是限制了你的选择,因为这种方法仍然可以通过线程实现. 无论如何,这完全是另一个与这里无关的讨论)。

3) 到目前为止,没有人提供任何真正的证据来说明为什么一种特定类型的上下文切换会比任何其他类型更耗时或更短。我在创建多任务内核(对于嵌入式控制器的小规模,没有什么比“真正的”操作系统更花哨)方面的经验表明,情况并非如此。

4)我迄今为止看到的所有旨在显示 Node 比其他网络服务器快得多的插图都存在严重缺陷,但是,它们的缺陷在某种程度上确实间接说明了我肯定会接受 Node 的一个优势(并且这绝不是微不足道的)。 Node 看起来不需要(实际上甚至也不允许)调整。如果您有线程模型,则需要创建足够的线程来处理预期负载。做得不好,你最终会表现不佳。如果线程太少,那么CPU是空闲的,但是无法接受更多的请求,创建的线程太多,会浪费内核内存,在Java环境下,也会浪费主堆内存.现在,对于 Java,浪费堆是破坏系统性能的第一个,最好的方法,因为高效的垃圾收集(目前,这可能会随着 G1 改变,但截至 2013 年初,陪审团似乎仍在这一点上至少)取决于有很多备用堆。所以,问题来了,用太少的线程调整它,你有空闲的 CPU 和很差的吞吐量,用太多的调整它,它会以其他方式陷入困境。

5) 还有另一种方式,我接受 Node 的方法“在设计上更快”的说法的逻辑,就是这样。大多数线程模型使用时间片上下文切换模型,分层在更合适(价值判断警报:)和更有效(不是价值判断)的抢占模型之上。发生这种情况有两个原因,首先,大多数程序员似乎不了解优先级抢占,其次,如果您在 windows 环境中学习线程,无论您喜欢与否,时间片都在那里(当然,这强化了第一点; 值得注意的是,Java 的第一个版本在 Solaris 实现中使用了优先级抢占,在 Windows 中使用了时间片。因为大多数程序员不理解并抱怨“线程在 Solaris 中不起作用”,所以他们将模型改为时间片)。无论如何,底线是时间片会创建额外的(并且可能是不必要的)上下文切换。每次上下文切换都会占用 CPU 时间,而该时间实际上已从可以在手头的实际工作中完成的工作中移除。但是,由于时间片在上下文切换上所花费的时间不应超过总时间的一小部分,除非发生了一些非常奇怪的事情,而且我没有理由期望在简单的网络服务器)。所以,是的,时间切片中涉及的过多上下文切换效率低下(这些通常不会发生在内核线程中,顺便说一句),但差异将是吞吐量的几个百分点,而不是隐含的整数因子在 Node.js 经常暗示的性能声明中。

无论如何,为这一切冗长而漫不经心道歉,但我真的觉得到目前为止,讨论还没有证明任何事情,我很高兴听到有人在这两种情况下的消息:

a) 对为什么 Node 应该更好的真实解释(除了我上面概述的两种情况,我相信第一种情况(调整不佳)是迄今为止我所看到的所有测试的真实解释。([编辑],实际上,我想得越多,我就越想知道大量堆栈使用的内存是否在这里很重要。现代线程的默认堆栈大小往往非常大,但是分配的内存基于闭包的事件系统只是需要的)

b) 一个真正的基准,它实际上为选择的线程服务器提供了公平的机会。至少这样,我不得不停止相信这些说法本质上是错误的;>([编辑]这可能比我预期的要强,但我确实觉得对性能优势的解释充其量是不完整的,而且显示的基准是不合理的)。

干杯,托比


线程的一个问题:它们需要 RAM。一个非常繁忙的服务器可以运行多达几千个线程。 Node.js 避免了线程,因此效率更高。效率不是通过更快地运行代码来实现的。代码是在线程中运行还是在事件循环中运行并不重要。对于 CPU 来说也是一样的。但是通过取消线程,我们节省了 RAM:只有一个堆栈而不是几千个堆栈。我们还保存了上下文切换。
但是节点并没有取消线程。它仍然在内部将它们用于 IO 任务,这是大多数 Web 请求所需要的。
节点还将回调的闭包存储在 RAM 中,所以我看不到它在哪里获胜。
@levi 但是nodejs 不使用“每个请求一个线程”之类的东西。它使用 IO 线程池,可能是为了避免使用异步 IO API 的复杂性(也许 POSIX open() 不能设为非阻塞?)。这样,它可以分摊传统 fork()/pthread_create()-on-request 模型必须创建和销毁线程的任何性能损失。而且,如后记 a) 中所述,这也摊销了堆栈空间问题。您可能可以使用 16 个 IO 线程来处理数千个请求。
“现代线程的默认堆栈大小往往非常大,但基于闭包的事件系统分配的内存只是需要的”我得到的印象是这些应该是相同的顺序。闭包并不便宜,运行时必须将单线程应用程序的整个调用树保留在内存中(可以说是“模拟堆栈”),并且当树的叶子作为关联闭包被释放时能够进行清理得到“解决”。这将包括大量对无法被垃圾收集的堆上内容的引用,并且会在清理时影响性能。
A
Alfred

我不明白的是 Node.js 仍然在使用线程。

Ryan 对阻塞的部分使用线程(大多数 node.js 使用非阻塞 IO),因为有些部分非常难以编写非阻塞。但我相信 Ryan 的愿望是让一切都没有阻塞。在 slide 63(internal design) 上,您会看到 Ryan 将 libev(抽象异步事件通知的库)用于非阻塞 eventloop。由于事件循环 node.js 需要较少的线程,从而减少了上下文切换、内存消耗等。


g
gawi

线程仅用于处理没有异步功能的函数,例如 stat()

stat() 函数始终处于阻塞状态,因此 node.js 需要使用线程来执行实际调用,而不会阻塞主线程(事件循环)。如果您不需要调用此类函数,则可能不会使用线程池中的任何线程。


B
BGerrissen

我对 node.js 的内部工作一无所知,但我可以看到使用事件循环如何胜过线程 I/O 处理。想象一个磁盘请求,给我 staticFile.x,为该文件创建 100 个请求。每个请求通常占用一个线程来检索该文件,即 100 个线程。

现在想象第一个请求创建一个成为发布者对象的线程,所有其他 99 个请求首先查看是否有 staticFile.x 的发布者对象,如果有,则在它工作时收听它,否则启动一个新线程并因此新的发布者对象。

一旦单个线程完成,它会将 staticFile.x 传递给所有 100 个侦听器并销毁自己,因此下一个请求会创建一个全新的线程和发布者对象。

所以在上面的例子中是 100 个线程与 1 个线程,但也是 1 个磁盘查找而不是 100 个磁盘查找,增益可能相当惊人。瑞恩是个聪明人!

另一种看待方式是他在电影开头的一个例子。代替:

pseudo code:
result = query('select * from ...');

同样,对数据库的 100 个单独查询与...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

如果一个查询已经在进行,其他相同的查询将简单地加入潮流,因此您可以在一次数据库往返中进行 100 个查询。


数据库的问题更多的是在阻止其他请求(可能使用也可能不使用数据库)的同时不等待答案的问题,而是要求一些东西,然后让它在它回来时给你打电话。我不认为它将它们联系在一起,因为这很难跟踪响应。另外,我认为没有任何 MySQL 接口可以让您在一个连接上保存多个无缓冲响应(??)
这只是一个抽象的例子来解释事件循环如何提供更高的效率,nodejs 在没有额外模块的情况下对 DB 什么都不做;)
是的,我的评论更多的是针对单个数据库往返中的 100 个查询。 :p
嗨 BGerrissen:好帖子。那么,当一个查询正在执行时,其他类似的查询会像上面的 staticFile.X 示例一样“监听”吗?例如,100 个用户检索相同的查询,只有一个查询将被执行,其他 99 个将听第一个?谢谢 !
你让它听起来像nodejs自动记忆函数调用或其他东西。现在,由于您不必担心 JavaScript 的事件循环模型中的共享内存同步,因此更容易安全地将内容缓存在内存中。但这并不意味着 nodejs 会神奇地为您做到这一点,或者这是被问及的性能增强类型。
A
Anurag Vohra

Node.JS 并不更快(也不意味着它更慢),但与处理其单线程的阻塞多线程系统相比,它在处理单线程方面效率很高!

我制作了图表来类比解释这个陈述。

https://i.stack.imgur.com/yLA5K.png

现在当然可以在阻塞的多线程系统(这就是 Node.js 的底层)之上构建一个非阻塞系统,但它非常复杂。而且您必须在需要非阻塞代码的地方执行此操作。

Javascript 生态系统(如 nodejs)提供了开箱即用的语法。 JS 语言 sytanx 在需要时提供了所有这些功能。此外,作为其语法的一部分,代码的读者会立即知道代码在哪里是阻塞的,在哪里是非阻塞的。

多线程阻塞系统的阻塞部分使其效率降低。被阻塞的线程在等待响应时不能用于其他任何事情。

而非阻塞单线程系统则充分利用了其单线程系统。