我刚刚观看了以下视频:Introduction to Node.js,但仍然不明白您是如何获得速度优势的。
主要是,Ryan Dahl(Node.js 的创建者)曾说过 Node.js 是基于事件循环的,而不是基于线程的。线程很昂贵,只能留给并发编程专家使用。
随后,他展示了 Node.js 的架构堆栈,该堆栈具有底层 C 实现,内部有自己的线程池。所以很明显,Node.js 开发人员永远不会启动他们自己的线程或直接使用线程池......他们使用异步回调。我就这么理解。
我不明白的是 Node.js 仍然在使用线程......它只是隐藏了实现,所以如果 50 个人请求 50 个文件(当前不在内存中),那么这如何更快,那么不需要 50 个线程?
唯一的区别是,由于它是在内部管理的,Node.js 开发人员不必编写线程细节,但在其下面仍然使用线程来处理 IO(阻塞)文件请求。
那么,您不是真的只是解决一个问题(线程)并在该问题仍然存在时隐藏它:主要是多线程、上下文切换、死锁……等等?
一定有一些细节我还是不明白。
select()
的操作系统级“切换”比线程上下文交换更快。
实际上这里有一些不同的东西被混为一谈。但它始于线程真的很难的模因。因此,如果它们很难,您更有可能在使用线程时 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。 (我不确定节点是否足够复杂,可以为同一个请求启动更多工作,但你明白了。)
笔记!这是一个古老的答案。虽然在粗略的轮廓上仍然如此,但由于 Node 在过去几年中的快速发展,一些细节可能已经发生了变化。
它使用线程是因为:
open() 的 O_NONBLOCK 选项不适用于文件。有些第三方库不提供非阻塞 IO。
要伪造非阻塞 IO,线程是必需的:在单独的线程中进行阻塞 IO。这是一个丑陋的解决方案,并导致大量开销。
在硬件层面上更糟:
使用 DMA,CPU 异步卸载 IO。
数据直接在 IO 设备和内存之间传输。
内核将其包装在一个同步的、阻塞的系统调用中。
Node.js 将阻塞系统调用包装在一个线程中。
这简直是愚蠢和低效的。但它至少有效!我们可以享受 Node.js,因为它隐藏了事件驱动的异步架构背后的丑陋和繁琐的细节。
也许将来有人会为文件实现 O_NONBLOCK ?...
编辑: 我和一位朋友讨论过这个问题,他告诉我,线程的替代方法是使用 select 轮询:指定超时 0 并在返回的文件描述符上执行 IO(现在它们得到保证不要阻止)。
我担心我在这里“做错事”,如果是这样,请删除我并道歉。特别是,我看不到我是如何创建一些人创建的整洁的小注释的。但是,我对这个线程有很多关注/观察。
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) 一个真正的基准,它实际上为选择的线程服务器提供了公平的机会。至少这样,我不得不停止相信这些说法本质上是错误的;>([编辑]这可能比我预期的要强,但我确实觉得对性能优势的解释充其量是不完整的,而且显示的基准是不合理的)。
干杯,托比
open()
不能设为非阻塞?)。这样,它可以分摊传统 fork()
/pthread_create()
-on-request 模型必须创建和销毁线程的任何性能损失。而且,如后记 a) 中所述,这也摊销了堆栈空间问题。您可能可以使用 16 个 IO 线程来处理数千个请求。
我不明白的是 Node.js 仍然在使用线程。
Ryan 对阻塞的部分使用线程(大多数 node.js 使用非阻塞 IO),因为有些部分非常难以编写非阻塞。但我相信 Ryan 的愿望是让一切都没有阻塞。在 slide 63(internal design) 上,您会看到 Ryan 将 libev(抽象异步事件通知的库)用于非阻塞 eventloop。由于事件循环 node.js 需要较少的线程,从而减少了上下文切换、内存消耗等。
线程仅用于处理没有异步功能的函数,例如 stat()
。
stat()
函数始终处于阻塞状态,因此 node.js 需要使用线程来执行实际调用,而不会阻塞主线程(事件循环)。如果您不需要调用此类函数,则可能不会使用线程池中的任何线程。
我对 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 个查询。
Node.JS 并不更快(也不意味着它更慢),但与处理其单线程的阻塞多线程系统相比,它在处理单线程方面效率很高!
我制作了图表来类比解释这个陈述。
https://i.stack.imgur.com/yLA5K.png
现在当然可以在阻塞的多线程系统(这就是 Node.js 的底层)之上构建一个非阻塞系统,但它非常复杂。而且您必须在需要非阻塞代码的地方执行此操作。
Javascript 生态系统(如 nodejs)提供了开箱即用的语法。 JS 语言 sytanx 在需要时提供了所有这些功能。此外,作为其语法的一部分,代码的读者会立即知道代码在哪里是阻塞的,在哪里是非阻塞的。
多线程阻塞系统的阻塞部分使其效率降低。被阻塞的线程在等待响应时不能用于其他任何事情。
而非阻塞单线程系统则充分利用了其单线程系统。