Node 有一个完全不同的范式,一旦它被正确捕获,就更容易看到这种不同的解决问题的方式。您永远不需要在 Node 应用程序中使用多个线程 (1),因为您有不同的方式来做同样的事情。您创建多个进程;但它与例如 Apache Web Server 的 Prefork mpm 的方式非常不同。
现在,假设我们只有一个 CPU 内核,我们将开发一个应用程序(以 Node 的方式)来完成一些工作。我们的工作是逐字节处理运行在其内容上的大文件。对于我们的软件来说,最好的方法是从文件的开头开始工作,然后一个字节一个字节地执行到最后。
-- 嘿,哈桑,我想你要么是个新手,要么是我祖父时代的老派!!!为什么不创建一些线程并使其更快?
-- 哦,我们只有一个 CPU 内核。
- 所以呢?创建一些线程的人,让它更快!
——它不是那样工作的。如果我创建线程,我会让它变慢。因为我将在线程之间切换的系统中增加大量开销,试图给它们一个公正的时间,在我的进程内部,试图在这些线程之间进行通信。除了所有这些事实之外,我还必须考虑如何将单个工作分成多个可以并行完成的部分。
——好吧好吧,我看你很穷。让我们用我的电脑,它有32个核心!
——哇,你真棒,我亲爱的朋友,非常感谢你。我很感激!
然后我们回去工作。现在我们有 32 个 cpu 核心,这要感谢我们富有的朋友。我们必须遵守的规则刚刚改变。现在我们想利用我们得到的所有这些财富。
要使用多核,我们需要找到一种方法将我们的工作分成可以并行处理的部分。如果不是 Node,我们将为此使用线程;32个线程,每个cpu核心一个。但是,由于我们有 Node,我们将创建 32 个 Node 进程。
线程可以是 Node 进程的一个很好的替代方案,甚至可能是更好的方法;但仅适用于已经定义了工作并且我们可以完全控制如何处理它的特定类型的工作。除此之外,对于所有其他类型的问题,即工作以我们无法控制的方式来自外部并且我们希望尽快回答,Node 的方式无疑是优越的。
-- 嘿,哈桑,你还在单线程工作吗?你怎么了,伙计?我刚刚为你提供了你想要的。你再也没有借口了。创建线程,让它运行得更快。
——我把工作分成几部分,每个过程都将并行处理其中一个部分。
-- 你为什么不创建线程?
——对不起,我认为它不可用。如果你愿意,你可以带上你的电脑吗?
-- 不,好吧,我很酷,我只是不明白你为什么不使用线程?
-- 谢谢你的电脑。:) 我已经将工作分成几部分,并创建了并行处理这些部分的流程。所有 CPU 内核都将得到充分利用。我可以用线程而不是进程来做到这一点;但是 Node 有这种方式,我的老板 Parth Thakkar 想让我使用 Node。
-- 好的,如果您需要另一台计算机,请告诉我。:p
如果我创建 33 个进程,而不是 32 个,操作系统的调度程序将暂停一个线程,启动另一个,在一些周期后暂停它,再次启动另一个......这是不必要的开销。我不想要这个。事实上,在一个有 32 个核心的系统上,我什至不想创建正好 32 个进程,31 可以更好。因为在这个系统上运行的不仅仅是我的应用程序。为其他事情留出一点空间可能会很好,尤其是如果我们有 32 个房间。
我相信我们现在就充分利用处理器处理CPU 密集型任务达成了一致。
——嗯,哈桑,有点儿嘲讽你很抱歉。我相信我现在更了解你了。但是我仍然需要解释一些事情:运行数百个线程的所有嗡嗡声是什么?我到处都读到线程比分叉进程更快创建和愚蠢?你 fork 进程而不是线程,你认为它是 Node 所能得到的最高值。那么 Node 是不是不适合这种工作呢?
——别担心,我也很酷。每个人都说这些话,所以我想我已经习惯了听到它们。
- 所以?Node 不适合这个?
-- Node 非常适合这个,尽管线程也可以很好。至于线程/进程创建开销;在你经常重复的事情上,每一毫秒都很重要。但是,我只创建了 32 个进程,这将花费很少的时间。它只会发生一次。它不会有任何区别。
-- 那我什么时候想创建数千个线程呢?
-- 你永远不想创建数千个线程。然而,在一个正在做来自外部的工作的系统上,比如一个处理 HTTP 请求的 Web 服务器;如果您为每个请求使用一个线程,您将创建很多线程,其中很多。
-- 节点不同,但是?对?
- 对,就是这样。这就是 Node 真正闪耀的地方。就像线程比进程轻得多,函数调用也比线程轻得多。节点调用函数,而不是创建线程。在 Web 服务器的示例中,每个传入请求都会导致函数调用。
——嗯,有趣;但如果您不使用多个线程,则只能同时运行一个函数。当大量请求同时到达 Web 服务器时,这如何工作?
- 你是完全正确的关于函数如何运行,一次一个,从不并行两个。我的意思是在一个进程中,一次只运行一个范围的代码。操作系统调度程序不会来暂停这个功能并切换到另一个,除非它暂停进程以给另一个进程时间,而不是我们进程中的另一个线程。(2)
-- 那么一个进程如何一次处理2个请求呢?
-- 只要我们的系统有足够的资源(RAM、网络等),一个进程一次可以处理数万个请求。这些功能的运行方式是关键区别。
——嗯,我现在应该兴奋吗?
-- 也许 :) 节点在队列上运行一个循环。在这个队列中是我们的工作,即我们开始处理传入请求的调用。这里最重要的一点是我们设计函数运行的方式。我们不是开始处理请求并让调用者等到我们完成工作,而是在完成可接受的工作量后迅速结束我们的函数。当我们需要等待另一个组件完成一些工作并返回一个值时,我们不需要等待,而是简单地完成我们的函数,将剩余的工作添加到队列中。
——听起来太复杂了?
——不不,我可能听起来很复杂;但是系统本身非常简单,而且非常有意义。
现在我想停止引用这两个开发人员之间的对话,并在最后一个关于这些功能如何工作的快速示例之后完成我的回答。
通过这种方式,我们正在做 OS Scheduler 通常会做的事情。我们在某个时候暂停我们的工作,让其他函数调用(如多线程环境中的其他线程)运行,直到我们再次轮到我们。这比把工作留给 OS 调度器要好得多,后者试图为系统上的每个线程分配时间。我们知道我们在做什么比 OS Scheduler 做得更好,并且我们应该在我们应该停止的时候停止。
下面是一个简单的示例,我们打开一个文件并读取它以对数据进行一些处理。
同步方式:
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)
通话;有时可能需要在耗时的任务期间休息一下,以允许队列中的呼叫有自己的处理份额。以不同的方式划分任务可以让你摆脱这些;但是,这并不是真正的 hack,它只是事件队列的工作方式。此外,process.nextTick
为此目的使用要好得多,因为当您使用时setTimeout
,需要计算和检查经过的时间,而process.nextTick
这正是我们真正想要的:“嘿任务,回到队列末尾,您已经使用了您的份额! "