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

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

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

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

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

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

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


当前回答

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

它使用线程是因为:

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

要伪造非阻塞IO,线程是必要的:在单独的线程中执行阻塞IO。这是一种丑陋的解决方案,会导致大量开销。

硬件层面的情况更糟:

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

这是愚蠢和低效的。但至少它是有效的!我们可以享受Node.js,因为它隐藏了事件驱动的异步架构背后丑陋和繁琐的细节。

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

编辑:我和一个朋友讨论过这个问题,他告诉我线程的另一种选择是使用select轮询:指定一个超时为0,并对返回的文件描述符执行IO(现在它们被保证不会阻塞)。

其他回答

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

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

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

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


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

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

我对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(现在它们被保证不会阻塞)。