当Node.js内部仍依赖于Threads时,其固有速度如何?


281

我刚刚观看了以下视频:Node.js简介,但仍然不了解如何获得速度优势。

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

后来,他然后展示了Node.js的体系结构栈,该体系结构栈具有基础的C实现,该实现在内部具有自己的线程池。因此,显然,Node.js开发人员永远不会启动自己的线程或直接使用线程池...他们使用异步回调。我很明白。

我不明白的是,Node.js仍在使用线程...只是在隐藏实现,因此,如果50个人很好地请求50个文件(当前不在内存中),那么不需要50个线程,这样做会更快吗? ?

唯一的区别是,由于它是在内部进行管理的,因此Node.js开发人员不必对线程详细信息进行编码,而是在其下方仍在使用线程来处理IO(阻止)文件请求。

因此,您难道不是真的只遇到一个问题(线程)并在该问题仍然存在时将其隐藏:主要是多个线程,上下文切换,死锁等吗?

必须有一些我仍然不明白的细节。


14
我倾向于同意您的观点,该要求有些过分简化。我认为节点的性能优势归结为两点:1)实际线程全部包含在较低的级别,因此在大小和数量上受到限制,从而简化了线程同步;2)通过操作系统级别的“切换” select()比线程上下文交换要快。
Pointy

Answers:


140

实际上,这里合并了一些不同的东西。但这始于模因,即线程真的很难。因此,如果它们很困难,则使用线程的可能性更大: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。(我不确定节点是否足够成熟,可以为同一请求启动更多工作,但是您明白了。)


6
好的,我可以肯定地看到这将如何提高性能,因为这听起来像您能够最大化CPU,因为没有任何线程或执行堆栈只是在等待IO返回,因此可以有效地找到Ryan所做的事情一种弥合所有差距的方法。
拉尔夫·卡拉维奥

34
是的,我要说的是,这并不是像他找到缩小差距的方法:这不是一种新模式。不同之处在于,他使用Javascript来让程序员以更方便这种异步的方式来表达他们的程序。可能是个
挑剔的

16
值得指出的是,对于许多I / O任务,Node使用可用的内核级异步I / O api(epoll,kqueue,/ dev / poll等)
Paul

7
我仍然不确定我是否完全理解它。如果我们认为在一个Web请求中,IO操作是处理请求所花费的大部分时间,并且为每个IO操作创建了一个新线程,那么对于50个快速连续的请求,我们将可能有50个线程并行运行并执行其IO部分。与标准Web服务器的不同之处在于,整个请求都在线程上执行,而在node.js中只是其IO部分,但这是花费大部分时间并使线程等待的部分。
Florin Dumitrescu

13
@SystemParadox感谢您指出这一点。我最近实际上对该主题进行了一些研究,确实发现,异步I / O在内核级别正确实现时,在执行异步I / O操作时不使用线程。而是在启动I / O操作后释放调用线程,并在I / O操作完成并且有线程可用时执行回调。因此,如果正确实现了对I / O操作的异步支持,那么node.js可以(几乎)使用一个线程(几乎)并行运行50个并发请求,同时执行50个I / O操作。
Florin Dumitrescu,

32

注意!这是一个老答案。尽管在粗略轮廓中仍然是正确的,但由于Node在过去几年中的快速发展,某些细节可能已更改。

使用线程是因为:

  1. open()O_NONBLOCK选项不适用于files
  2. 有些第三方库不提供非阻塞IO。

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

在硬件级别,甚至更糟:

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

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

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

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


Windows呢?
Pacerier '17

对不起,不知道 我只知道libuv是进行异步工作的平台无关层。在Node的开头没有libuv。然后决定拆分libuv,这使平台特定的代码更容易。换句话说,Windows有其自己的异步故事,该故事可能与Linux完全不同,但是对我们而言这并不重要,因为libuv为我们做了艰苦的工作。
凌晨

28

我担心我在这里“做错了事”,如果要删除我,我深表歉意。特别是,我看不到如何创建某些人创建的简洁的小注释。但是,我对此线程有很多担忧/观察。

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时间,并且该时间被有效地从可以完成的实际工作中删除了。但是,由于时间分段,在上下文切换上投入的时间不应超过总时间的很小一部分,除非发生了一些非常古怪的事情,而且我没有理由可以预期这种情况会发生。简单的网络服务器)。因此,是的,时间分段中涉及的多余上下文切换效率很低(并且这些不会在 从而有效地将时间从可以完成的实际工作中省去了。但是,由于时间分段,在上下文切换上投入的时间不应超过总时间的很小一部分,除非发生了一些非常古怪的事情,而且我没有理由可以预期这种情况会发生。简单的网络服务器)。因此,是的,时间分段中涉及的多余上下文切换效率很低(并且这些不会在 从而有效地将时间从可以完成的实际工作中省去了。但是,由于时间分段,在上下文切换上投入的时间不应超过总时间的很小一部分,除非发生了一些非常古怪的事情,而且我没有理由可以预期这种情况会发生。简单的网络服务器)。因此,是的,时间分段中涉及的多余上下文切换效率很低(并且这些不会在顺便说一句,内核线程通常是吞吐量),但差异将是吞吐量的百分之几,而不是节点通常暗示的性能要求中暗示的整数因子。

无论如何,对于所有这些都是漫长而粗鲁的道歉,但是我真的感觉到到目前为止,讨论还没有证明任何事情,在以下两种情况下,我都很高兴听到有人的来信:

a)关于Node为什么要更好的真实解释(除了我上面概述的两种情况之外,我认为其中第一个(调整不佳)是到目前为止我所见过的所有测试的真实解释。 ],实际上,我考虑得越多,我就越想知道大量堆栈使用的内存在这里是否有意义。现代线程的默认堆栈大小往往非常大,但是由a分配的内存基于闭包的事件系统仅是需要的)

b)一个真正的基准,实际上给选择的线程服务器一个公平的机会。至少以这种方式,我不得不停止相信这些声明本质上是虚假的;显示的基准是不合理的)。

干杯,托比


2
线程有问题:它们需要RAM。一个非常繁忙的服务器可以运行多达数千个线程。Node.js避免了线程,因此效率更高。效率不是通过更快地运行代码来实现的。代码是在线程中运行还是在事件循环中运行都没有关系。对于CPU来说是相同的。但是,通过消除线程,我们可以节省RAM:仅一个堆栈,而不是数千个堆栈。并且我们还保存上下文切换。
2013年

3
但是节点并没有消除线程。仍然在内部将它们用于IO任务,这是大多数Web请求所需要的。
莱维,2014年

1
另外,节点将回调的关闭存储在RAM中,因此我看不到它在哪里获胜。
Oleksandr Papchenko,2015年

@levi但是,nodejs并不使用“每个请求一个线程”之类的东西。它使用IO线程池,可能是为了避免使用异步IO API带来的复杂性(也许open()不能使POSIX 成为非阻塞的?)。通过这种方式,它摊销任何性能损失在传统fork()/ pthread_create()-on请求模型必须创建和销毁线程。并且,如后文a)中所述,这也分摊了堆栈空间问题。您可以使用16个IO线程来满足数千个请求。
宾基

“现代线程的默认堆栈大小往往非常大,但是基于闭包的事件系统分配的内存仅是所需的”,我的印象是这些应该具有相同的顺序。闭包并不便宜,运行时将必须将单线程应用程序的整个调用树保存在内存中(可以说是“模拟堆栈”),并且当关联的闭包被释放时,将能够清除树的叶子得到“解决”。这将包括大量无法处理的堆载内容的引用,这些内容将在清理时影响性能。
David Tonhofer

14

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

Ryan将线程用于阻塞的部分(大部分node.js使用非阻塞IO),因为某些部分难以编写非阻塞的疯狂。但是我相信Ryan希望一切都畅通无阻。在幻灯片63(内部设计)上,您看到Ryan将libev(抽象化异步事件通知的库)用于非阻塞eventloop。由于事件循环,node.js需要较少的线程,从而减少了上下文切换,内存消耗等。


11

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

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


7

我对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个查询。


3
数据库问题更多的是一个问题,即在等待其他请求(可能使用或可能不使用数据库)的同时不等待答案,而是要问一些问题,然后等它回来时再打电话给您。我认为它不会将它们链接在一起,因为要跟踪响应非常困难。我也不认为有任何MySQL接口可以让您在一个连接上保留多个无缓冲响应(??)
Tor Valamo 2010年

这只是一个抽象的示例,它解释了事件循环如何提高效率,Nodejs在没有额外模块的情况下对DB
无效

1
是的,我的评论更多是针对单个数据库往返中的100个查询。:p
Tor Valamo

2
嗨,BGerrissen:好的帖子。因此,当执行查询时,其他类似的查询将像上面的staticFile.X示例一样“监听”?例如,有100个用户检索相同的查询,则仅将执行一个查询,而其他99个将在侦听第一个查询?谢谢 !
CHAPA 2011年

1
您听起来好像nodejs自动记住了函数调用之类的东西。现在,由于您不必担心JavaScript事件循环模型中的共享内存同步,因此更容易安全地将内容缓存在内存中。但这并不意味着nodejs会为您神奇地做到这一点,也不意味着这是所要求的性能增强类型。
宾基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.