我刚刚看了下面的视频:Node.js介绍,仍然不明白你是如何获得速度优势的。

主要是,Ryan Dahl (Node.js的创造者)说Node.js是基于事件循环的,而不是基于线程的。线程的开销很大,只能留给并发编程专家使用。

随后,他展示了Node.js的架构堆栈,其中有一个底层C实现,在内部有自己的线程池。所以很明显Node.js开发者永远不会启动他们自己的线程或者直接使用线程池…它们使用异步回调。这点我能理解。

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

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

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

这里一定有一些细节我还是不明白。


我对node.js的内部工作原理一无所知,但我可以看到如何使用事件循环可以胜过线程I/O处理。想象一个磁盘请求,给我一个staticFile。X,为该文件设置100个请求。每个请求通常占用一个线程来检索该文件,也就是100个线程。

现在想象一下,第一个请求创建了一个线程,该线程成为一个发布者对象,所有其他99个请求首先查看staticFile是否有一个发布者对象。X,如果是,在它工作时监听它,否则启动一个新线程,从而创建一个新的publisher对象。

一旦单个线程完成,它将传递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个查询。


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

Ryan使用线程处理阻塞的部分(大多数node.js使用非阻塞IO),因为有些部分很难编写非阻塞。但我相信瑞恩的愿望是一切都不阻塞。 在第63页(内部设计)中,您可以看到Ryan使用libev(抽象异步事件通知的库)来实现非阻塞事件循环。由于事件循环,node.js需要更少的线程,从而减少上下文切换,内存消耗等。


实际上这里有一些不同的东西被合并在一起。但它始于一个迷因,那就是线程真的很难。所以如果它们很难,你更有可能在使用线程时,1)由于bug而中断,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(现在它们被保证不会阻塞)。


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

stat()函数总是阻塞的,所以node.js需要使用一个线程来执行实际调用,而不阻塞主线程(事件循环)。如果不需要调用这类函数,那么线程池中的线程可能永远不会被使用。


我担心我在这里“做错了事情”,如果是这样,删除我,我道歉。特别是,我不知道我是如何创建一些人已经创建的整洁的小注释的。然而,我对这个话题有很多关注/观察。

1)热门答案之一伪代码中的注释元素

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

本质上是虚假的。如果线程正在计算,那么它就不是在无所事事,而是在做必要的工作。另一方面,如果它只是等待IO的完成,那么它没有使用CPU时间,内核中线程控制基础设施的全部意义在于CPU会找到一些有用的事情来做。正如本文所建议的那样,“无所事事”的唯一方法是创建一个轮询循环,而编写过真正web服务器代码的人都不可能做到这一点。

2) "Threads are hard", only makes sense in the context of data sharing. If you have essentially independent threads such as is the case when handling independent web requests, then threading is trivially simple, you just code up the linear flow of how to handle one job, and sit pretty knowing that it will handle multiple requests, and each will be effectively independent. Personally, I would venture that for most programmers, learning the closure/callback mechanism is more complex than simply coding the top-to-bottom thread version. (But yes, if you have to communicate between the threads, life gets really hard really fast, but then I'm unconvinced that the closure/callback mechanism really changes that, it just restricts your options, because this approach is still achievable with threads. Anyway, that's a whole other discussion that's really not relevant here).

3)到目前为止,还没有人提出任何真实的证据来说明为什么一种特定类型的上下文切换比其他类型的上下文切换更费时或更省时。我在创建多任务内核方面的经验(在小型嵌入式控制器上,没有什么比“真正的”操作系统更花哨的了)表明情况不会是这样的。

4) All the illustrations that I have seen to date that purport to show how much faster Node is than other webservers are horribly flawed, however, they're flawed in a way that does indirectly illustrate one advantage I would definitely accept for Node (and it's by no means insignificant). Node doesn't look like it needs (nor even permits, actually) tuning. If you have a threaded model, you need to create sufficient threads to handle the expected load. Do this badly, and you'll end up with poor performance. If there are too few threads, then the CPU is idle, but unable to accept more requests, create too many threads, and you will waste kernel memory, and in the case of a Java environment, you'll also be wasting main heap memory. Now, for Java, wasting heap is the first, best, way to screw up the system's performance, because efficient garbage collection (currently, this might change with G1, but it seems that the jury is still out on that point as of early 2013 at least) depends on having lots of spare heap. So, there's the issue, tune it with too few threads, you have idle CPUs and poor throughput, tune it with too many, and it bogs down in other ways.

5) There is another way in which I accept the logic of the claim that Node's approach "is faster by design", and that is this. Most thread models use a time-sliced context switch model, layered on top of the more appropriate (value judgement alert :) and more efficient (not a value judgement) preemptive model. This happens for two reasons, first, most programmers don't seem to understand priority preemption, and second, if you learn threading in a windows environment, the timeslicing is there whether you like it or not (of course, this reinforces the first point; notably, the first versions of Java used priority preemption on Solaris implementations, and timeslicing in Windows. Because most programmers didn't understand and complained that "threading doesn't work in Solaris" they changed the model to timeslice everywhere). Anyway, the bottom line is that timeslicing creates additional (and potentially unnecessary) context switches. Every context switch takes CPU time, and that time is effectively removed from the work that can be done on the real job at hand. However, the amount of time invested in context switching because of timeslicing should not be more than a very small percentage of the overall time, unless something pretty outlandish is happening, and there's no reason I can see to expect that to be the case in a simple webserver). So, yes, the excess context switches involved in timeslicing are inefficient (and these don't happen in kernel threads as a rule, btw) but the difference will be a few percent of throughput, not the kind of whole number factors that are implied in the performance claims that are often implied for Node.

无论如何,很抱歉我说了这么长时间,但我真的觉得,到目前为止,讨论还没有证明任何东西,我很高兴听到有人在这些情况下说:

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

B)一个真正的基准测试,实际上给线程服务器选择一个公平的机会。至少这样,我就不必再相信这些声明本质上是错误的;>([编辑]这可能比我想的要强烈得多,但我确实觉得对性能好处给出的解释充其量是不完整的,所显示的基准是不合理的)。

欢呼, 托比


Node.JS并不更快(并不意味着它更慢),但在处理单个线程时非常高效,与处理单个线程的阻塞多线程系统相比!

我已经做了图表,用类比来解释这句话。

现在当然可以在阻塞多线程系统(这就是Node.js的本质)之上构建一个非阻塞系统,但它非常复杂。你必须在任何需要非阻塞代码的地方这样做。

Javascript生态系统(如nodejs)提供了开箱即用的语法。JS语言sytanx在任何需要的地方都提供了所有这些特性。此外,作为语法的一部分,代码的读者可以立即知道代码在哪里阻塞,在哪里非阻塞。


多线程阻塞系统的阻塞部分降低了系统的效率。被阻塞的线程在等待响应期间不能用于其他任何事情。

而非阻塞单线程系统则充分利用了它的单线程系统。