我刚刚看了下面的视频:Node.js介绍,仍然不明白你是如何获得速度优势的。
主要是,Ryan Dahl (Node.js的创造者)说Node.js是基于事件循环的,而不是基于线程的。线程的开销很大,只能留给并发编程专家使用。
随后,他展示了Node.js的架构堆栈,其中有一个底层C实现,在内部有自己的线程池。所以很明显Node.js开发者永远不会启动他们自己的线程或者直接使用线程池…它们使用异步回调。这点我能理解。
我不明白的是Node.js仍然在使用线程…它只是隐藏的实现,所以这是如何更快,如果50人请求50个文件(目前不在内存中),那么不需要50个线程?
唯一的区别是,由于它是内部管理的,Node.js开发人员不必编写线程细节,但在底层它仍然使用线程来处理IO(阻塞)文件请求。
所以你真的只是把一个问题(线程)隐藏起来,而这个问题仍然存在:主要是多线程,上下文切换,死锁……等等?
这里一定有一些细节我还是不明白。
实际上这里有一些不同的东西被合并在一起。但它始于一个迷因,那就是线程真的很难。所以如果它们很难,你更有可能在使用线程时,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.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在过去几年的快速发展,一些细节可能已经发生了变化。
它使用线程是因为:
open()的O_NONBLOCK选项对文件不起作用。
有些第三方库不提供非阻塞IO。
要伪造非阻塞IO,线程是必要的:在单独的线程中执行阻塞IO。这是一种丑陋的解决方案,会导致大量开销。
硬件层面的情况更糟:
使用DMA, CPU异步卸载IO。
数据直接在IO设备和内存之间传输。
内核将其封装在一个同步的、阻塞的系统调用中。
Node.js将阻塞系统调用包装在一个线程中。
这是愚蠢和低效的。但至少它是有效的!我们可以享受Node.js,因为它隐藏了事件驱动的异步架构背后丑陋和繁琐的细节。
也许将来有人会为文件实现O_NONBLOCK ?…
编辑:我和一个朋友讨论过这个问题,他告诉我线程的另一种选择是使用select轮询:指定一个超时为0,并对返回的文件描述符执行IO(现在它们被保证不会阻塞)。
实际上这里有一些不同的东西被合并在一起。但它始于一个迷因,那就是线程真的很难。所以如果它们很难,你更有可能在使用线程时,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。(我不确定节点是否足够复杂,可以为相同的请求启动更多的工作,但您可以理解。)