当Node.js仍然依赖于内部的线程时,Node.js如何更快地运行?
我只是看了下面的video: 介绍Node.js ,仍然不明白你如何获得速度的好处。
主要的一点是Ryan Dahl(Node.js的创build者)说Node.js是基于事件循环而不是基于线程的。 线程是昂贵的,只能留给并发编程专家来利用。
后来,他显示了Node.js的架构栈,它有一个底层的C实现,在内部有自己的线程池。 所以显然,Node.js开发者永远不会开始自己的线程或直接使用线程池…他们使用asynchronouscallback。 我理解这一点。
我不明白的一点是,Node.js仍然在使用线程……它只是隐藏了实现,所以如果50个人请求50个文件(当前不在内存中),那么这个速度如何更快,而不是50个线程?
唯一的区别是,由于它在内部进行pipe理,Node.js开发人员不必编写线程细节,但在其下面仍然使用线程来处理IO(阻止)文件请求。
所以你不是真的只是一个问题(线程),并隐藏它,而这个问题仍然存在:主要是multithreading,上下文切换,死锁…等?
必须有一些细节,我仍然不明白在这里。
实际上这里有几个不同的东西被混合在一起。 但是,从线索开始真的很难。 所以,如果他们很难,你更有可能,当使用线程1)由于错误中断和2)不尽可能有效地使用它们。 (2)是你问的那个。
想想他给出的一个例子,请求进入的地方,然后运行一些查询,然后对结果进行处理。 如果以标准的程序方式编写代码,代码可能如下所示:
result = query( "select smurfs from some_mushroom" ); // twiddle fingers go_do_something_with_result( result );
如果进来的请求导致你创build一个运行上面的代码的新线程,那么你将有一个线程坐在那里,在query()
运行时什么也不做。 (根据Ryan的说法,Apache使用单个线程来满足原始请求,而nginx在他谈论的情况下performance优于它,因为它不是)。
现在,如果你真的很聪明,你可以用一种方式来expression上面的代码,当你运行查询的时候,环境可能会停止运行,
query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );
这基本上是node.js正在做的事情。 你基本上是装饰的 – 由于语言和环境的缘故,这是一个方便的方式,因此关于closures的点 – 你的代码使得环境可以聪明地运行什么,什么时候运行。 这样,node.js在发明asynchronousI / O(不是任何人声称这样)的意义上是不新的,但是它的performance方式有点不同。
注意:当我说环境可以聪明的运行什么的时候,具体来说我的意思是说它用来启动一些I / O的线程现在可以用来处理一些其他的请求,或者一些可以做的计算并行,或启动一些其他并行I / O。 (我不确定节点是否足够复杂,可以为同样的请求启动更多的工作,但是你明白了。)
注意! 这是一个古老的答案。 虽然在粗略的概述中仍然如此,但是由于Node在过去几年的快速发展,一些细节可能已经发生了变化。
它使用线程,因为:
- open()的O_NONBLOCK选项在文件上不起作用 。
- 有第三方库不提供非阻塞IO。
要伪造非阻塞IO,线程是必需的:在单独的线程中阻塞IO。 这是一个丑陋的解决scheme,造成很大的开销。
在硬件上更糟糕的是:
- 通过DMA ,CPUasynchronous卸载IO。
- 数据直接在IO设备和内存之间传输。
- 内核将其封装在一个同步的阻塞系统调用中。
- Node.js将阻塞系统调用包装在一个线程中。
这简直是愚蠢而低效。 但它至less起作用! 我们可以享受Node.js,因为它隐藏了事件驱动的asynchronous架构背后的丑陋和繁琐的细节。
也许有人会在未来的文件实现O_NONBLOCK?…
编辑:我与朋友讨论了这一点,他告诉我,线程的替代方法是轮询与select :指定超时为0,并在返回的文件描述符(现在,他们保证不阻止)做IO。
我担心我会在这里“做错事”,如果是这样的话,请删除我,我道歉。 特别是,我看不出我是如何创造出一些人们创造的简洁的小注释。 不过,我对这个主题有很多关注/意见。
1)stream行答案之一的伪代码中的注释元素
result = query( "select smurfs from some_mushroom" ); // twiddle fingers go_do_something_with_result( result );
本质上是假的。 如果线程正在计算,那么它不是在大拇指,它正在做必要的工作。 另一方面,如果它只是在等待IO的完成,那么就不使用CPU时间,内核中的线程控制基础结构的整个重点就是CPU将find一些有用的工作。 这里build议的“旋转拇指”的唯一方法是创build一个轮询循环,没有人编写真正的networking服务器是不够的。
2)“线程很难”,只有在数据共享的情况下才有意义。 如果您拥有基本上独立的线程(如处理独立Web请求的情况),那么线程非常简单,您只需编写如何处理一个工作的线性stream程,并且知道它将处理多个请求,将是有效的独立。 就个人而言,我敢打赌,对于大多数程序员来说,学习闭包/callback机制比简单编码顶级线程版本更复杂。 (但是,是的,如果你必须在线程之间进行交stream,生活真的很难实现,但是我不确定闭包/callback机制真的改变了,它只是限制了你的select,因为这个方法仍然可以通过线程实现无论如何,这是一个其他的讨论,在这里是不相关的)。
3)到目前为止,还没有人提出任何真正的证据来certificate为什么一种特定types的上下文切换比其他任何types的切换更耗时或更less。 我在创build多任务内核方面的经验(embedded式控制器的小规模,没有像“真正的”操作系统那么高级)表明,事实并非如此。
4)迄今为止我所看到的所有插图都表明节点比其他的networking服务器要快得多,但是它们的缺陷是间接地说明了我肯定会接受Node的一个优点(和这绝不是微不足道的)。 节点看起来并不需要(甚至不允许,实际上)调整。 如果你有一个线程模型,你需要创build足够的线程来处理预期的负载。 这样做很糟糕,最终会导致性能下降。 如果线程太less,则CPU空闲,但无法接受更多的请求,创build太多的线程,并且会浪费内核内存,而在Java环境的情况下,也会浪费主堆内存。 现在,对于Java来说,浪费堆是第一个,也是最好的方式来搞砸系统的性能,因为高效的垃圾收集(目前,这可能会改变与G1,但似乎在2013年初,评审团仍然在这一点上至less)取决于有很多备用堆。 所以,出现这个问题,调整线程太less,CPU空闲,吞吐量不足,调整太多,并且以其他方式陷入困境。
5)还有另外一种方法可以让我接受Node的方法“通过devise更快”的说法的逻辑,就是这样。 大多数线程模型使用时间切片的上下文切换模型,在更合适的(值判断警报:)和更高效的(不是值判断)抢先模型之上分层。 发生这种情况有两个原因,第一,大多数程序员似乎没有理解优先权抢占,第二,如果你在Windows环境下学习线程,无论你喜欢与否,时间片都在那里(当然,这加强了第一点;值得注意的是,Java的第一个版本在Solaris实现上使用了抢占优先权,在Windows中使用了timelicing,因为大多数程序员不理解和抱怨“线程在Solaris中不能工作”,所以他们把模型转换到了时间片上。 无论如何,底线是时间片创build额外的(可能是不必要的)上下文切换。 每个上下文切换都需要CPU时间,并且这个时间可以从实际工作中可以完成的工作中有效地移除。 但是,由于时间片的原因,投入到上下文切换中的时间不应该超过整个时间的很小一部分,除非发生了一些相当古怪的事情,而且我没有理由期望在这种情况下简单的networking服务器)。 所以,是的,在时间片中涉及的多余的上下文切换是低效的(并且这些在内核线程中通常不会发生,顺便说一下),但是差异将是吞吐量的百分之几,而不是暗示的整数因子在性能索赔往往暗示为节点。
无论如何,对于这一切都表示歉意,我们真的感觉到,迄今为止,讨论没有任何证据,我很乐意听到有人在这两种情况之一:
a)真正的解释,为什么Node应该更好(超出上面提到的两个场景,其中第一个(糟糕的调整)我相信是迄今为止所见到的所有testing的真实解释。 ],实际上,我越想越多,我想知道是否大量堆栈使用的内存可能是重要的。现代线程的默认堆栈大小往往是相当巨大的,但由内存分配的内存基于闭包的事件系统将是唯一需要的)
b)一个真正的基准,实际上给了select的线程服务器一个公平的机会。 至less这样,我不得不停止相信这些声明本质上是错误的;>(这可能比我想要的要强,但是我确实认为给出的性能好处的解释最多是不完整的,显示的基准是不合理的)。
干杯,托比
我不明白的是,Node.js仍然在使用线程。
Ryan使用线程来阻止那些被阻塞的部分(大多数node.js使用了非阻塞IO),因为有些部分疯狂地难以写非阻塞。 但我相信瑞恩的愿望是让所有东西都不被阻挡。 在幻灯片63(内部devise)中,您会看到Ryan使用libev (抽象asynchronous事件通知的库)用于非阻塞事件callback 。 由于事件循环node.js需要较less的线程,这减less了上下文切换,内存消耗等。
线程仅用于处理没有asynchronous设施的函数,如stat()
。
stat()
函数总是阻塞的,所以node.js需要使用一个线程来执行实际的调用而不阻塞主线程(事件循环)。 如果你不需要调用这些函数,潜在地,线程池中的任何线程都不会被使用。
我对node.js的内部工作一无所知,但我可以看到如何使用事件循环可以超越线程I / O处理。 想象一下,一个光盘请求,给我staticFile.x,使其100个请求该文件。 每个请求通常占用一个线程retreiving该文件,即100线程。
现在设想第一个请求创build一个成为发布者对象的线程,所有其他99个请求首先查看是否存在staticFile.x的发布者对象,如果是,则在它正在工作时监听它,否则启动一个新线程,新的发布者对象。
一旦单线程完成,它将staticFile.x传递给所有100个监听器并销毁自己,所以下一个请求会创build一个新的线程和发布者对象。
所以在上面的例子中,它是100个线程对1个线程,而是1个盘片查找而不是100个盘片查找,这个增益可能是非常有用的。 瑞安是一个聪明的人!
另一种看待的方式是在电影开始时的一个例子。 代替:
pseudo code: result = query('select * from ...');
再次,100个单独的查询到数据库与…:
pseudo code: query('select * from ...', function(result){ // do stuff with result });
如果一个查询已经完成,其他同样的查询就会跳到这一行,所以你可以在一次数据库往返中有100个查询。