我刚观看了以下视频:Introduction to Node.js但仍然不明白你如何获得速度优势。
主要有一点,Ryan Dahl(Node.js的创建者)说Node.js是基于事件循环而不是基于线程的。线程很昂贵,应该只留给并发编程的专家来使用。
稍后,他将展示Node.js的体系结构堆栈,它具有底层C实现,内部具有自己的线程池。所以显然Node.js开发人员永远不会启动他们自己的线程或直接使用线程池...他们使用异步回调。我明白了。
我不明白的是,Node.js仍在使用线程...它只是隐藏了实现,所以如果有50个人请求50个文件(目前不在内存中),那么这个怎么会更快呢?需要50个线程?
唯一的区别是,由于它是在内部管理的,因此Node.js开发人员不必编写线程细节的代码,但在其下面仍然使用线程来处理IO(阻塞)文件请求。
所以你是不是真的只是遇到一个问题(线程)并在问题仍然存在时将其隐藏起来:主要是多线程,上下文切换,死锁......等等?
必须有一些我仍然不明白的细节。
答案 0 :(得分:137)
实际上有一些不同的东西在这里被混淆。但它始于线程真的很难的模因。因此,如果它们很难,则更有可能在使用线程时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不是 new ,因为它发明了异步I / O(并不是有人声称这样的东西),但它是新的,因为它表达的方式是一点点不同。
注意:当我说环境可以巧妙地了解运行的时间和时间时,具体我的意思是它用于启动某些I / O的线程现在可用于处理其他一些请求,或者某些计算可以并行完成,也可以启动其他并行I / O. (我不确定节点是否足够复杂,可以为同一个请求开始更多的工作,但是你明白了。)
答案 1 :(得分:32)
注意!这是一个老答案。虽然在粗略的轮廓中仍然如此,但由于Node在过去几年的快速发展,一些细节可能已经改变。
正在使用线程,因为:
要伪造非阻塞IO,必须使用线程:在单独的线程中阻塞IO。这是一个丑陋的解决方案,并导致很多开销。
在硬件级别上更糟糕:
这简直是愚蠢而且效率低下。但它至少起作用了!我们可以享受Node.js,因为它隐藏了事件驱动的异步架构背后的丑陋和繁琐的细节。
将来有人会为文件实现O_NONBLOCK吗?...
编辑:我和一位朋友讨论了这个问题,他告诉我线程的替代方法是使用select进行轮询:指定超时0并对返回的文件描述符执行IO(现在他们保证不会阻止。)
答案 2 :(得分:28)
我担心我做错了什么"在这里,如果是这样删除我,我道歉。特别是,我没有看到我如何创建一些人创建的整洁的小注释。但是,我在这个帖子上有很多关注/意见。
1)其中一个流行答案中的伪代码中的注释元素
result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );
基本上是虚假的。如果线程是计算的,那么它不是在嘲笑它,它正在做必要的工作。另一方面,如果它只是等待IO的完成,那么<em>不使用CPU时间,内核中线程控制基础结构的重点是CPU将找到有用的东西。唯一可以扭动你的拇指的方法。这里建议的是创建一个轮询循环,没有人编写真正的网络服务器就不足以做到这一点。
2)&#34;线程很难&#34;,只有在数据共享的环境中才有意义。如果您有基本上独立的线程,例如处理独立Web请求时的情况,那么线程很简单,您只需编写如何处理一个作业的线性流程,并且非常知道它将处理多个请求,并且每个将是有效独立的。就个人而言,对于大多数程序员来说,我会冒险,学习闭包/回调机制比简单编写从上到下的线程版本更复杂。 (但是,是的,如果你必须在线程之间进行通信,生活变得非常困难,但是我不相信闭包/回调机制真的改变了它,它只是限制你的选择,因为这种方法仍然可以实现与线程。无论如何,这是另一个与此无关的讨论。)
3)到目前为止,没有人提供任何真实的证据证明为什么一种特定类型的上下文切换比任何其他类型更多或更少时间消耗。我在创建多任务内核方面的经验(在嵌入式控制器的小规模上,没有像真正的&#34;操作系统那么花哨)表明情况并非如此。 4)迄今为止我所看到的所有插图都表明节点比其他网络服务器的速度快得多,但是,它们的确存在缺陷,间接地说明了我肯定会有一个优势接受节点(它并不是无关紧要的)。节点看起来不需要(甚至不允许,实际上)调整。如果您有线程模型,则需要创建足够的线程来处理预期的负载。这样做很糟糕,你最终会表现不佳。如果线程太少,那么CPU处于空闲状态,但无法接受更多请求,创建太多线程,并且您将浪费内核内存,而在Java环境中,您也会浪费主堆内存。现在,对于Java来说,浪费堆是第一个,最好的方式来搞砸系统的性能,因为有效的垃圾收集(目前,这可能会随着G1而改变,但似乎陪审团仍然在这一点上截至2013年初至少)取决于拥有大量备用堆。因此,存在问题,用太少的线程调整它,你有空闲的CPU和低吞吐量,用太多调整它,并且在其他方面陷入困境。5)我还有另一种方式可以接受Node的说法逻辑,即Node&#34;更快,设计更快,就是这样。大多数线程模型使用时间切片的上下文切换模型,在更合适的(值判断警报:)和更高效(不是值判断)抢先模型之上分层。出现这种情况有两个原因,首先,大多数程序员似乎都不了解优先级抢占,其次,如果你在windows环境中学习线程,那么无论你喜欢与否,都会出现时间限制(当然,这会加强第一点;值得注意的是,Java的第一个版本在Solaris实现上使用了优先级抢占,在Windows中使用了时间限制。因为大多数程序员都不理解并且抱怨线程在Solaris和#34;中不起作用。他们把模型变成了时间片段。无论如何,底线是时间片创建了额外的(并且可能是不必要的)上下文切换。每个上下文切换都占用CPU时间,并且该时间可以有效地从可以在手边的实际工作上完成的工作中移除。然而,由于时间的推移而投入上下文切换的时间不应超过总时间的很小一部分,除非发生一些非常古怪的事情,并且我没有理由认为这是简单的网络服务器中的情况)。所以,是的,时间限制中涉及的多余上下文切换是低效的(并且这些通常不会发生在内核线程中,顺便说一句,但是差异将是吞吐量的百分之几,而不是性能声明中隐含的整数因子,通常隐含在Node中。
无论如何,对于所有这些都是长篇大论而道歉,但我真的觉得到目前为止,讨论还没有证明什么,我很乐意听到有人在这两种情况中听到:
a)真正解释为什么Node应该更好(除了我上面概述的两个场景,我认为第一个(调整不佳)是我所有测试的真正解释。到目前为止看到的。([编辑],实际上,我越是想到它,我就越想知道大量堆栈使用的内存是否在这里很重要。现代线程的默认堆栈大小往往是非常庞大,但基于闭包的事件系统分配的内存只是需要的内容
b)真正的基准测试,实际上为所选的线程服务器提供了公平的机会。至少就是这样,我不得不停止相信这些说法基本上是假的;&gt; ([编辑]这可能比我想要的更强,但我确实认为对性能优势的解释充其量是不完整的,所显示的基准是不合理的。)
干杯, 托比
答案 3 :(得分:14)
我不明白的是重点 Node.js仍在使用线程。
Ryan对阻塞的部分使用线程(大多数node.js使用非阻塞IO),因为有些部分很难写非阻塞。但我相信瑞恩希望一切都是无阻碍的。 在slide 63(internal design)上,您看到Ryan使用libev(抽象异步事件通知的库)用于非阻塞eventloop。由于事件循环node.js需要较少的线程,这减少了上下文切换,内存消耗等。
答案 4 :(得分:11)
线程仅用于处理没有异步功能的函数,如stat()
。
stat()
函数总是阻塞,因此node.js需要使用线程来执行实际调用而不阻塞主线程(事件循环)。如果您不需要调用这些函数,则可能不会使用线程池中的任何线程。
答案 5 :(得分:7)
我对node.js的内部工作原理一无所知,但我可以看到使用事件循环可以胜过线程I / O处理。想象一下光盘请求,给我staticFile.x,对它提出100个请求。每个请求通常会占用一个线程来检索该文件,即100个线程。
现在假设第一个请求创建一个成为发布者对象的线程,所有99个其他请求首先查看是否有staticFile.x的发布者对象,如果是,请在它正在执行时听取它,否则启动一个新线程因此是一个新的发布者对象。
一旦完成单个线程,它就会将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个查询。