Node具有完全不同的范例,一旦正确掌握,便更容易看到这种不同的解决问题方式。您永远不需要在Node应用程序中使用多个线程(1),因为您有一种不同的做事方式。您创建多个进程;但它与Apache Web服务器的Prefork mpm的工作方式非常非常不同。
现在,让我们想象我们只有一个CPU核心,并且我们将开发一个应用程序(以Node的方式)来完成一些工作。我们的工作是处理一个大文件,逐字节运行其中的内容。对于我们的软件来说,最好的方法是从文件开头开始工作,按字节跟随到结尾。
-- 嗨,Hasan,我猜你要么是新手,要么是我祖父那个时代的老派人物!!! 为什么不创建一些线程,让它快得多呢?
- -哦,我们只有一个CPU核心。
-- 那又怎样?创建一些线程,让它更快!
-- 不是这样的。如果我创建线程,那么速度会变慢。因为我会向系统添加大量开销以在线程之间切换,试图给它们恰当的时间,并在我的过程中试图在这些线程之间通信。除了所有这些事实外,我还必须考虑如何将单个任务分成多个可以并行完成的部分。
-- 好的好的,我明白你很穷。让我们使用我的电脑,它有32个核心!
--哇,你太棒了,亲爱的朋友,非常感谢你。我很感激!
然后我们回到工作中。现在我们有32个CPU核心,这要归功于我们富有的朋友。我们必须遵守的规则已经改变。现在我们想利用得到的所有财富。
要使用多个核心,我们需要找到一种将工作分成可以并行处理的片段的方法。如果不是Node,我们会使用线程;32个线程,每个CPU核心一个。然而,既然我们有了Node,我们将创建32个Node进程。
线程可能是Node进程的一个很好的替代品,甚至可能是一种更好的方式;但只适用于一类特定的工作,其中工作已经定义好了,我们完全控制如何处理它。除此之外,对于其他任何来自外部的问题,我们无法控制工作方式并且想要尽快回答的问题,Node的方式无疑是优越的。
-- 嗨,Hasan,你还在单线程工作吗?你怎么了,伙计?我给你提供了你想要的东西。你再也没有借口了。创建线程,让它运行得更快。
-- 我已将工作分成了多个部分,每个进程都会并行处理其中的一个部分。
-- 为什么不创建线程?
-- 抱歉,我认为这是不可用的。如果你愿意,可以拿走你的电脑吗?
-- 不,好的,我很酷,我只是不明白为什么你不使用线程?
--谢谢你提供的计算机。 :)我已将工作分成多个部分,并创建了进程以并行处理这些部分。所有CPU核心都将得到充分利用。我也可以使用线程来完成这项工作;但是Node有自己的方法,而我的老板Parth Thakkar希望我使用Node。
--好的,如果你需要另一台计算机,请告诉我。:p
--如果我创建33个进程而不是32个,则操作系统的调度程序将暂停一个线程,启动另一个线程,在一些周期后再次暂停它,然后再启动另一个线程......这是不必要的开销。我不希望出现这种情况。实际上,在一个有32个内核的系统上,我甚至不想创建32个进程,31个进程可能会更好。因为不仅是我的应用程序会在这个系统上工作。留出一些空间给其他程序可能是有好处的,特别是如果我们有32个房间。
--我相信我们现在对于充分利用处理器进行CPU密集型任务已经达成了共识。
--嗯,Hasan,对于我有点嘲笑你,我感到很抱歉。我相信我现在更好地理解你了。但仍有一些事情需要解释:大量运行线程的热潮是什么?我到处都看到线程比分叉进程更快、更简单?你使用进程而不是线程来分叉,认为Node的性能已经达到了最高点。那么Node是否不适合这种工作?
--没问题,我很冷静。每个人都这样说,所以我觉得我已经习惯了这种听话。
--那么?Node不适合这个吗?
--即使线程也很好,但是对于线程/进程创建开销;在你重复许多次的事情上,每毫秒都很重要。但是,我仅创建32个进程,这将需要极少量的时间。它只会发生一次。这并不会造成任何影响。
--那么何时需要创建数千个线程?
--你永远不需要创建数千个线程。但是,在处理来自外部的工作的系统上,如处理HTTP请求的Web服务器;如果您为每个请求使用一个线程,则会创建大量线程,其中许多线程是无用的。
--但Node不同?对吧?
--是的,正是如此。这就是Node真正出色的地方。就像线程比进程轻,函数调用比线程轻。Node调用函数,而不是创建线程。在Web服务器的例子中,每个传入的请求都会引起一个函数调用。
--嗯,有趣;但如果您不使用多个线程,一次只能运行一个函数,当许多请求同时到达Web服务器时,如何处理?
-- 你关于函数运行的说法是完全正确的,一次只能运行一个函数,永远不会同时运行两个函数。也就是说,在单个进程中,一次只能运行一个代码范围。除非操作系统调度程序暂停该函数并切换到另一个函数,否则不会暂停该函数并切换到另一个函数,除非暂停该进程以给另一个进程(而不是我们进程中的另一个线程)留出时间。(2)
-- 那么进程如何处理两个请求?
-- 只要我们的系统有足够的资源(RAM、网络等),进程就可以同时处理成千上万个请求。这些函数的运行方式是关键的区别。
-- 嗯,现在我应该感到兴奋吗?
-- 可能 :) Node在队列上运行循环。在这个队列中,我们的工作是我们开始处理传入请求的调用。这里最重要的一点是我们设计函数运行的方式。我们不会像其他人一样开始处理请求,并让调用者等待直到我们完成工作,而是在完成了可接受的工作后迅速结束我们的函数。当我们需要等待另一个组件执行一些工作并返回值时,我们不会等待它,而是简单地完成我们的函数,将其余的工作添加到队列中。
-- 听起来太复杂了吗?
-- 不不,我可能听起来很复杂,但这个系统本身非常简单,而且非常有道理。
现在我想停止引用这两个开发人员之间的对话,并在最后快速举一个例子来说明这些函数如何工作。
通过这种方式,我们正在做操作系统调度程序通常会做的事情。我们在某个时刻暂停我们的工作,让其他函数调用(例如多线程环境中的其他线程)运行,直到我们再次轮到我们为止。这比将工作留给操作系统调度程序要好得多,后者试图为系统上的每个线程提供时间。我们比操作系统调度程序更清楚自己在做什么,并且我们应该在应该停止的时候停止。
下面是一个简单的示例,我们打开一个文件并读取它以对数据进行一些处理。
同步方式:
Open File
Repeat This:
Read Some
Do the work
异步方式:
Open File and Do this when it is ready: // Our function returns
Repeat this:
Read Some and when it is ready: // Returns again
Do some work
正如你所看到的,我们的函数要求系统打开一个文件,并且不等待它被打开。当文件准备好后,它会提供下一步操作并自行完成。当我们返回时,Node会在队列上运行其他函数调用。在运行完所有函数后,事件循环会移动到下一个轮回...
总之,Node有一个完全不同于多线程开发的范式,但这并不意味着它缺少功能。对于同步作业(我们可以决定处理顺序和方式),它的性能与多线程并行一样好。对于来自外部的作业,例如对服务器的请求,它更加优越。
(1) 除非你正在构建其他语言的库,比如C/C++,否则你仍然不需要创建线程来分割任务。对于这种工作,你有两个线程,其中一个将继续与Node通信,而另一个则执行实际工作。
(2) 实际上,每个Node进程都有多个线程,原因如我在第一个脚注中提到的那样。然而,这绝不像1000个线程做类似的工作。这些额外的线程是用来接受IO事件和处理进程间消息传递等事务的。
更新(作为对评论中一个好问题的回复)
@Mark,感谢您的建设性批评。在Node的范式中,除非队列中的所有其他调用都被设计为一个接一个地运行,否则不应该有需要太长时间来处理的函数。对于计算开销大的任务,如果我们从整体上看这个问题,我们会发现这不是一个“我们应该使用线程还是进程”的问题,而是一个“我们如何将这些任务以平衡的方式分成子任务,以便我们可以在系统上利用多个CPU核心并行运行它们?”的问题。假设我们要在一个有8个核心的系统上处理400个视频文件。如果我们想一次只处理一个文件,那么我们需要一个系统,它会处理同一文件的不同部分,在这种情况下,也许一个多线程单进程系统会更容易构建,甚至更高效。我们仍然可以通过运行多个进程并在必要时在它们之间传递消息来使用Node进行此操作。正如我之前所说的,对于这种类型的任务,Node的多进程方法与多线程方法一样好;但不会更好。再次强调,Node闪耀的情况是当这些任务作为输入同时从多个源流入系统时,因为在Node中保持许多连接并发相比于每个连接一个线程或一个进程系统更轻量级。
关于
setTimeout(...,0)
调用; 有时在耗时的任务中给予一些休息,以允许队列中的调用有机会进行处理是必要的。将任务划分为不同的方式可以使您免受这些问题的困扰;但是,这并不是一种真正的技巧,它只是事件队列工作的方式。此外,为了实现这个目的,使用
process.nextTick
更好,因为当您使用
setTimeout
时,需要计算和检查经过的时间,而
process.nextTick
就是我们真正想要的:“嘿,任务,回到队列末尾,你已经用完你的份额了!”