当Node.js仍然依赖于内部线程时,Node.js如何更快地发挥作用?

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

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

后来,他显示了Node.js的架构栈,它有一个底层的C实现,它内部有自己的线程池。 所以很显然,Node.js开发人员不会直接启动他们自己的线程或直接使用线程池......他们使用异步回调。 我理解这一点。

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

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

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

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


实际上这里有几个不同的东西被混合在一起。 但是从线索真的很难的模因开始。 所以,如果他们很难,你更有可能,当使用线程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)迄今为止我看到的所有插图都表明节点比其他网络服务器速度快很多,但是,它们的缺陷在于间接说明了我肯定会接受节点的一个优点(和这绝不是微不足道的)。 节点看起来并不需要它(甚至不允许,实际上)调整。 如果你有一个线程模型,你需要创建足够的线程来处理预期的负载。 做得很糟糕,最后你的表现会很差。 如果线程太少,则CPU处于空闲状态,但无法接受更多请求,创建太多线程,并且会浪费内核内存,而在Java环境中,您也将浪费主堆内存。 现在,对于Java而言,浪费堆是第一个,也是最好的方法来破坏系统的性能,因为高效的垃圾回收(目前,这可能会随着G1而改变,但似乎到2013年初,评审团仍然处于此阶段至少)取决于有很多备用堆。 所以,存在这个问题,用线程太少来调整它,你有CPU空闲和吞吐量不足,调整太多,并且以其他方式陷入困境。

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

    无论如何,我们对所有这些都表示歉意,但我确实感到迄今为止,讨论没有任何证据,我很乐意在这些情况下听到有人说:

    a)真正的解释,为什么Node应该更好(超出我上面概述的两种情况,其中第一种情况(糟糕的调优),我相信是迄今为止所见过的所有测试的真实解释。 ],实际上,我考虑得越多,我越想知道大量堆栈使用的内存在这里可能是否有意义。现代线程的默认堆栈大小往往非常大,但是由内存分配的内存基于闭包的事件系统将只是需要的)

    b)一个真正的基准,实际上为所选线程服务器提供了一个公平的机会。 至少这样,我不得不停止相信这些声明本质上是错误的;>([编辑]可能比我想要的更强,但我确实认为给出的性能优势的解释最多是不完整的,而且显示的基准是不合理的)。

    干杯,托比

    链接地址: http://www.djcxy.com/p/49147.html

    上一篇: How is Node.js inherently faster when it still relies on Threads internally?

    下一篇: C# EWS Api on Exchange 2013 : Get an attachment into a Stream