Node.js在内部依赖Threads时本身如何更快?

Ral*_*veo 279 javascript architecture concurrency node.js

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

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

之后,他展示了Node.js的体系结构堆栈,它具有底层的C实现,内部有自己的Thread池.显然,Node.js开发人员永远不会启动自己的线程或直接使用线程池...他们使用异步回调.我明白了.

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

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

那么你是不是真的只是遇到一个问题(线程)并在问题仍然存在时将其隐藏起来:主要是多线程,上下文切换,死锁......等等?

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

jrt*_*ton 137

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

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

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );
Run Code Online (Sandbox Code Playgroud)

如果进入的请求导致您创建一个运行上述代码的新线程,那么您将有一个线程坐在那里,在query()运行时根本不执行任何操作.(根据Ryan的说法,Apache正在使用单个线程来满足原始请求,而nginx在他谈论的情况下表现优于它,因为它不是.)

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

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );
Run Code Online (Sandbox Code Playgroud)

这基本上就是node.js正在做的事情.你基本上是在装修 - 以一种方便的方式,因为语言和环境,因此关于闭包的要点 - 你的代码以这样的方式环境可以巧妙地运行什么,何时运行.通过这种方式,node.js 在发明异步I/O的意义上并不是的(不是任何人声称这样的东西),但它的新方式表达方式有点不同.

注意:当我说环境可以巧妙地了解运行的时间和时间时,特别是我的意思是它用来启动某些I/O的线程现在可以用来处理其他一些请求,或者可以做一些计算并行,或启动一些其他并行I/O. (我不确定节点是否足够复杂,可以为同一个请求开始更多的工作,但是你明白了.)

  • 是的,我要说的一件事是,他并没有找到缩小差距的方法:这不是一个新的模式.不同的是,他正在使用Javascript让程序员以一种对这种异步更方便的方式表达他们的程序.可能是一个挑剔的细节,但仍然...... (34认同)
  • 值得指出的是,对于很多I/O任务,Node使用任何可用的内核级异步I/O api(epoll,kqueue,/ dev/poll,等等) (16认同)
  • @SystemParadox感谢你指出这一点.实际上我最近对这个主题进行了一些研究,实际上,问题是异步I/O在内核级别正确实现时,在执行异步I/O操作时不使用线程.而是在I/O操作启动后立即释放调用线程,并在I/O操作完成并且线程可用时执行回调.因此,如果正确实现了对I/O操作的异步支持,node.js可以使用一个线程在(几乎)并行中运行50个并发请求和50个I/O操作. (13认同)
  • 我仍然不确定我是否完全理解它.如果我们考虑在Web请求中IO操作是占用处理请求所需的大部分时间的操作,并且如果为每个IO操作创建一个新线程,那么对于50个非常快速连续的请求,我们将可能有50个线程并行运行并执行其IO部分.与标准Web服务器的区别在于,整个请求在线程上执行,而在node.js中只是其IO部分,但这是大部分时间占用并使线程等待的部分. (7认同)
  • 好吧,我可以肯定地看到这会如何提高性能,因为它听起来像你能够最大化你的CPU,因为没有任何线程或执行堆栈只是等待IO返回所以Ryan已经做了有效的发现一种弥合所有差距的方法. (6认同)
  • @FlorinDumitrescu,区别在于50个IO操作可能不使用50个线程 - 它们可以以合理的方式排队.当留给内核时这是特别好的,正如Paul所指出的那样,通常就是这种情况. (5认同)
  • @FlorinDumitrescu,我认为并非所有IO操作都会释放线程.只有网络IO才会释放线程并由IO硬件负责.但是在fs IO的情况下,它必须从池中获取一个新线程并执行.http://docs.libuv.org/en/latest/design.html ..所以我现在有同样的怀疑..将50请求不产生50个线程...现在上下文切换的问题是它不回来了吗?困惑 (3认同)
  • 究竟!node.js的性能不是由于它的基于事件的循环或一些异步的io,大大缩短等待时间的回调系统是node.js性能的核心. (2认同)

nal*_*ply 32

注意!这是一个古老的答案.虽然在粗略的轮廓中仍然如此,但由于Node在过去几年的快速发展,一些细节可能已经改变.

它正在使用线程,因为:

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

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

它在硬件级别上更糟糕:

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

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

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

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


Tob*_*itt 28

我担心我在这里"做错了",如果是这样,请删除我,我道歉.特别是,我没有看到我如何创建一些人创建的整洁的小注释.但是,我在这个帖子上有很多关注/意见.

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

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );
Run Code Online (Sandbox Code Playgroud)

基本上是虚假的.如果线程正在计算,那么它不是在大拇指,它正在做必要的工作.另一方面,如果它只是等待IO的完成,那么它使用CPU时间,内核中线程控制基础结构的重点是CPU将找到一些有用的东西.像这里建议的那样"旋转你的拇指"的唯一方法是创建一个轮询循环,没有人编写一个真正的网络服务器就不足以做到这一点.

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

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

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

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

无论如何,为这一切都很漫长和愚蠢道歉,但我真的觉得到目前为止,讨论还没有证明什么,我很高兴听到有人在这两种情况中:

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

b)真正的基准测试,实际上为所选的线程服务器提供了公平的机会.至少在那种情况下,我不得不停止相信这些说法基本上是假的;>([编辑]这可能比我想要的更强,但我确实认为对性能优势的解释充其量是不完整的,而且显示的基准是不合理的).

干杯,托比

  • 但节点并没有消除线程.它仍然在内部使用它们来执行IO任务,这是大多数Web请求所需要的. (3认同)
  • 线程有问题:他们需要RAM.一台非常繁忙的服务器最多可以运行几千个线程.Node.js避免了线程,因此效率更高.效率不是通过更快地运行代码.代码是在线程中还是在事件循环中运行并不重要.对于CPU来说,它是一样的.但是通过消除线程我们节省了RAM:只有一个堆栈而不是几千个堆栈.我们还保存了上下文切换. (2认同)

Alf*_*red 14

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

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


gaw*_*awi 11

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

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


BGe*_*sen 7

我一无所知node.js的内部工作,但我可以看到如何使用一个事件循环可以超越线程I/O处理.想象一下光盘请求,给我staticFile.x,对它提出100个请求.每个请求通常占用一个线程检索该文件,即100个线程.

现在想象一下,第一个请求创建一个线程成为出版商的对象,所有99个其他请求首先要看是否有对应staticFile.x出版商对象,如果是的话,听它,而它在做它的工作,否则启动一个新线程,因此新的发布者对象.

一旦一个线程完成,它通过staticFile.x所有100个听众和破坏本身,因此下一个请求创建一个全新的线程和发行对象.

因此,这100个线程VS在上面的例子中1线,而且还1个查找,而不是100个查找,增益可以说是相当phenominal.瑞恩是个聪明人!

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

pseudo code:
result = query('select * from ...');
Run Code Online (Sandbox Code Playgroud)

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

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

如果查询已走,其他等于查询只会赶时髦,所以你可以在一个单一的数据库往返100个查询.

  • 数据库的问题更多的是在等待其他请求(可能使用或不使用数据库)时不等待答案的问题,而是要求某些东西,然后让它在回来时给你打电话.我不认为它将它们联系在一起,因为跟踪响应是非常困难的.另外我不认为有任何MySQL接口可以让你在一个连接上持有多个无缓冲的响应(??) (3认同)
  • 嗨BGerrissen:好帖子.那么,当一个查询正在执行时,其他类似的查询将像上面的staticFile.X示例一样"监听"?例如,100个用户检索相同的查询,只执行一个查询,其他99个将在第一个查询?谢谢 ! (2认同)
  • 你让它听起来像nodejs自动记住函数调用什么的。现在,因为您不必担心 JavaScript 事件循环模型中的共享内存同步,所以可以更轻松地将内容安全地缓存在内存中。但这并不意味着 Nodejs 会神奇地为您做到这一点,也不意味着这就是所要求的性能增强类型。 (2认同)