当Node.js内部仍然依赖线程时,它是如何固有地更快的?

我刚刚看了下面的视频:Node.js简介,仍然不明白你是如何得到速度的好处。

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

随后,他展示了Node.js的架构堆栈,其中有一个底层C实现,在内部有自己的线程池。所以很明显Node.js开发者永远不会启动他们自己的线程或者直接使用线程池…它们使用异步回调。这点我能理解。

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

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

所以你真的只是把一个问题(线程)隐藏起来,而这个问题仍然存在:主要是多线程,上下文切换,死锁……等等?

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

41802 次浏览

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

现在想象一下,第一个请求创建了一个线程,该线程成为一个发布者对象,所有其他99个请求首先查看staticFile是否有一个发布者对象。X,如果是,在它工作时监听它,否则启动一个新线程,从而创建一个新的publisher对象。

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

因此,在上面的例子中,它是100个线程vs 1个线程,但也是1个磁盘查找而不是100个磁盘查找,增益是非常显著的。瑞恩是个聪明人!

另一种方法是看他在电影开头的一个例子。而不是:

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

同样,100个独立的数据库查询与…

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

如果一个查询已经在运行,其他相同的查询将简单地跟随潮流,因此在单个数据库往返中可以有100个查询。

我不明白的是重点

.js仍然在使用线程
Ryan使用线程处理阻塞的部分(大多数node.js使用非阻塞IO),因为有些部分很难编写非阻塞。但我相信瑞恩的愿望是一切都不阻塞。 在幻灯片63(内部设计)上,你可以看到Ryan使用libev(抽象异步事件通知的库)来实现非阻塞的eventloop。由于事件循环,node.js需要更少的线程,这减少了上下文切换,内存消耗等

实际上这里有一些不同的东西被合并在一起。但它始于一个迷因,那就是线程真的很难。所以如果它们很难,你更有可能在使用线程时,1)由于bug而中断,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在过去几年的快速发展,一些细节可能已经发生了变化。

它使用线程是因为:

  1. open()的O_NONBLOCK选项对文件不起作用
  2. 有些第三方库不提供非阻塞IO。

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

硬件层面的情况更糟:

  • 使用直接存储器存取, CPU异步卸载IO。
  • 数据直接在IO设备和内存之间传输。
  • 内核将其封装在一个同步的、阻塞的系统调用中。
  • Node.js将阻塞系统调用包装在一个线程中。

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

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

我和一个朋友讨论过这个问题,他告诉我线程的另一种选择是轮询选择:指定一个超时为0,并对返回的文件描述符进行IO(现在它们被保证不会阻塞)。

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

stat()函数总是阻塞的,所以node.js需要使用一个线程来执行实际调用,而不阻塞主线程(事件循环)。如果不需要调用这类函数,那么线程池中的线程可能永远不会被使用。

我担心我在这里“做错了事情”,如果是这样,删除我,我道歉。特别是,我不知道我是如何创建一些人已经创建的整洁的小注释的。然而,我对这个话题有很多关注/观察。

1)热门答案之一伪代码中的注释元素

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

本质上是虚假的。如果线程正在计算,那么它就不是在无所事事,而是在做必要的工作。另一方面,如果它只是等待IO的完成,那么它使用CPU时间,内核中线程控制基础设施的全部意义在于CPU会找到一些有用的事情来做。正如本文所建议的那样,“无所事事”的唯一方法是创建一个轮询循环,而编写过真正web服务器代码的人都不可能做到这一点。

2) “线程很难”,只有在数据共享上下文中才有意义。如果你有本质上独立的线程,比如在处理独立的web请求时,那么线程就很简单了,你只需要编写如何处理一个任务的线性流程,就可以很好地知道它将处理多个请求,而且每个请求都是有效独立的。我个人认为,对于大多数程序员来说,学习闭包/回调机制比简单地编写从上到下的线程版本要复杂得多。(但是,是的,如果你必须在线程之间通信,生活变得非常困难,非常快,但我不相信闭包/回调机制真的改变了这一点,它只是限制了你的选择,因为这种方法仍然可以用线程实现。不管怎样,这是另一个完全无关紧要的讨论)。

3)到目前为止,还没有人提出任何真实的证据来说明为什么一种特定类型的上下文切换比其他类型的上下文切换更费时或更省时。我在创建多任务内核方面的经验(在小型嵌入式控制器上,没有什么比“真正的”操作系统更花哨的了)表明情况不会是这样的。

4)到目前为止,我所看到的所有旨在展示Node比其他web服务器快多少的插图都存在严重的缺陷,然而,它们的缺陷在某种程度上间接说明了我肯定会接受Node的一个优势(这绝不是微不足道的)。Node看起来不需要(实际上甚至不允许)调优。如果您有一个线程模型,您需要创建足够的线程来处理预期的负载。做得不好,你就会以糟糕的表现结束。如果线程太少,则CPU处于空闲状态,但无法接受更多请求,创建太多线程,将浪费内核内存,在Java环境中,还将浪费主堆内存。现在,对于Java来说,浪费堆是破坏系统性能的第一个也是最好的方法,因为有效的垃圾收集(目前,这可能会随着G1而改变,但至少在2013年初,这一点似乎还没有定论)依赖于拥有大量的空闲堆。因此,这就是问题所在,在线程太少的情况下进行调优,您有空闲的cpu和低吞吐量,在线程太多的情况下进行调优,它会在其他方面陷入困境。

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

无论如何,很抱歉我说了这么长时间,但我真的觉得,到目前为止,讨论还没有证明任何东西,我很高兴听到有人在这些情况下说:

a)真正解释为什么Node应该更好(除了我上面概述的两个场景之外,我认为第一个(糟糕的调优)是迄今为止我看到的所有测试的真正解释。([编辑],实际上,我想得越多,我就越想知道大量堆栈使用的内存在这里是否重要。现代线程的默认堆栈大小往往相当大,但由基于闭包的事件系统分配的内存将只是所需要的。)

B)一个真正的基准测试,实际上给线程服务器选择一个公平的机会。至少这样,我就不必再相信这些声明本质上是错误的;>([编辑]这可能比我想的要强烈得多,但我确实觉得对性能好处给出的解释充其量是不完整的,所显示的基准是不合理的)。

< p >欢呼, 托比< / p >

Node.JS并不更快(并不意味着它更慢),但是高效地处理单个线程,与处理它的单线程的阻塞多线程系统相比!

我已经做了图表,用类比来解释这句话。

enter image description here enter image description here < / p >

现在当然可以在阻塞多线程系统(这就是Node.js的本质)之上构建一个非阻塞系统,但它非常复杂。你必须在任何需要非阻塞代码的地方这样做。

Javascript生态系统(如nodejs)提供了开箱即用的语法。JS语言sytanx在任何需要的地方都提供了所有这些特性。此外,作为语法的一部分,代码的读者可以立即知道代码在哪里阻塞,在哪里非阻塞。


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

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