Node有一个完全不同的范例,一旦它被正确捕获,就更容易看到这种解决问题的不同方式.在一个 node 应用程序中永远不需要多个线程(1),因为做同一件事的方式不同.创建多个流程;但它与ApacheWeb服务器的Prefork mpm非常不同.
现在,让我们假设我们只有一个CPU核心,我们将开发一个应用程序(以 node 的方式)来做一些工作.我们的工作是逐字节处理一个大文件的内容.对于我们的软件来说,最好的方法是从文件的开头开始工作,一个字节一个字节地跟踪到最后.
--嘿,哈桑,我想你要么是个新手,要么是我祖父时代的老派!!!为什么不创建一些线程并使其更快呢?
--哦,我们只有一个CPU核心.
--那又怎样?创建一些线程,让它更快!
--它不是那样工作的.如果我创建线程,我会让它变慢.因为我会给系统增加很多开销,以便在线程之间切换,给它们一点时间,在我的进程中,try 在这些线程之间进行通信.除了所有这些事实,我还必须考虑如何将一项工作划分为多个可以并行完成的部分.
--好吧,我看你很穷.让我们用我的电脑吧,它有32个核!
--哇,你真棒,我亲爱的朋友,非常感谢.我很感激!
然后我们再回go 工作.多亏了我们富有的朋友,现在我们有了32个cpu核心.我们必须遵守的规则刚刚改变.现在我们想利用我们得到的所有财富.
要使用多核,我们需要找到一种方法,将我们的工作划分为可以并行处理的部分.如果不是 node ,我们将使用线程来实现这一点;32个线程,每个cpu核心一个.然而,由于我们有 node ,我们将创建32个 node 进程.
线程可以是 node 进程的一个很好的替代方案,甚至可能是更好的方式;但只有在一种特定的工作中,工作已经确定,我们完全可以控制如何处理它.除此之外,对于所有其他类型的问题,如果工作来自外部,而我们无法控制,并且希望尽快回答,那么Node的方式无疑是优越的.
--嘿,哈桑,你还在用单线程吗?你怎么了,伙计?我刚刚给你提供了你想要的.你再也没有借口了.创建线程,使其运行更快.
--我已经把作品分成了几个部分,每个过程都会在其中一个部分上并行工作.
--为什么不创建线程呢?
--对不起,我认为它不可用.如果你愿意,你可以带上你的电脑?
--不,好吧,我很酷,我只是不明白你为什么不使用线程?
--谢谢你的电脑.:)我已经把工作分成了几个部分,我创建了并行处理这些部分的流程.所有的CPU核心都将得到充分利用.我可以用线程而不是进程来实现这一点;但Node有这种方式,我的老板Parth Thakkar希望我使用Node.
--好的,如果你需要另一台电脑,请告诉我P
如果我创建33个进程,而不是32个,操作系统的调度程序将暂停一个线程,启动另一个,在一些周期后暂停,再次启动另一个...这是不必要的开销.我不要.事实上,在一个32核的系统上,我甚至不想创建32个进程,31个可以是nicer个.因为在这个系统上工作的不仅仅是我的应用程序.给其他东西留一点空间是很好的,尤其是如果我们有32个房间的话.
我相信我们现在在CPU-intensive tasks个处理器的充分利用上是一致的.
--哈桑,我很抱歉嘲笑了你一点.我相信我现在更了解你了.但我仍然需要一个解释:运行数百个线程的所有嗡嗡声是什么?我到处都读到,线程的创建速度要比Forking 进程快得多,而且很愚蠢?您Forking 进程而不是线程,并且您认为这是使用Node可以获得的最高值.那么Node不适合这种工作吗?
--别担心,我也很酷.每个人都说这些话,所以我想我已经习惯了.
--所以呢?Node不适合这个?
--Node在这方面非常好,尽管线程也可以很好.至于线程/进程创建开销;对于你经常重复的事情,每毫秒都很重要.然而,我只创建了32个进程,只需要很少的时间.它只会发生一次.这不会有任何区别.
--那么,我想什么时候创建数千个线程呢?
--你永远不想创建成千上万的线程.然而,在一个正在做来自外部的工作的系统上,比如处理HTTP请求的web服务器;如果您对每个请求使用一个线程,您将创建很多线程,其中很多.
--但 node 不同吗?正当
--没错.这个 node 真正发光的地方.就像线程比进程轻得多一样,函数调用也比线程轻得多. node 调用函数,而不是创建线程.在web服务器的示例中,每个传入的请求都会导致函数调用.
--嗯,很有趣;但如果不使用多个线程,则只能同时运行一个函数.当大量请求同时到达web服务器时,这怎么可能起作用?
--关于函数如何运行,您完全正确,一次一个,从来没有两个函数并行运行.我的意思是,在一个进程中,一次只运行一个代码范围.OS调度器不会来暂停此函数并切换到另一个函数,除非它暂停进程以给另一个进程(而不是进程中的另一个线程)留出时间.(2)
--那么一个进程如何一次处理两个请求呢?
--只要我们的系统有足够的资源(RAM、网络等),一个进程一次可以处理数万个请求.这些功能的运行方式是关键区别.
--嗯,我现在应该兴奋吗?
--可能:) node 在队列上运行循环.在这个队列中是我们的工作,即我们开始处理传入请求的呼叫.这里最重要的一点是我们设计函数运行的方式.我们没有开始处理请求,让调用者等到我们完成任务,而是在完成了可接受的工作量后迅速结束我们的函数.当我们需要等待另一个组件完成一些工作并返回一个值时,我们不需要等待,只需完成将剩余工作添加到队列中的函数.
--听起来太复杂了?
--不,不,我可能听起来很复杂;但这个系统本身非常简单,非常合理.
现在我不想再引用这两个开发者之间的对话了,我想在最后一个简单的例子之后完成我的回答.
通过这种方式,我们正在做OS调度器通常会做的事情.我们在某个时刻暂停工作,让其他函数调用(如多线程环境中的其他线程)运行,直到再次轮到我们为止.这比把工作交给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与多线程开发有着完全不同的范式;但这并不意味着它缺少东西.对于同步作业(job)(我们可以决定处理的顺序和方式),它与多线程并行性一样有效.对于一个来自外部的工作,比如对服务器的请求,它就更优越了.
(1) 除非您正在用C/C++等其他语言构建库,否则在这种情况下,您仍然不会创建用于分割作业(job)的线程.对于这类工作,您有两个线程,其中一个线程将继续与Node通信,而另一个线程执行实际工作.
(2) 事实上,每个 node 进程都有多个线程,原因与我在第一个脚注中提到的相同.然而,这并不像1000个线程做类似的工作.这些额外的线程用于接受IO事件和处理进程间消息传递.
更新(作为对 comments 中一个好问题的回复)
@马克,谢谢你的建设性批评.在Node的范例中,除非队列中的所有其他调用都被设计为一个接一个地运行,否则永远不应该有处理时间过长的函数.对于计算成本很高的任务,如果我们完整地查看图片,我们会发现这不是"我们应该使用线程还是进程"的问题但问题是"我们如何以一种平衡的方式将这些任务划分为子任务,以便在系统上使用多个CPU核并行运行它们?"假设我们将在一个8核的系统上处理400个视频文件.如果我们想一次处理一个文件,那么我们需要一个系统来处理同一文件的不同部分.在这种情况下,多线程单进程系统可能更容易构建,甚至更高效.我们仍然可以通过运行多个进程并在需要状态共享/通信时在它们之间传递消息来使用Node.如前所述,在此类任务中, node 为as well as的多进程方法是多线程方法;但仅此而已.同样,正如我之前所说的,当我们将这些任务作为输入从多个源输入到系统中时, node 的优势在于,与每个连接的线程或每个连接的进程相比,在 node 中同时保持多个连接要轻得多.
至于setTimeout(...,0)
个电话;有时,在一项耗时的任务中,可能需要休息一下,以便让队列中的呼叫有自己的处理份额.以不同的方式划分任务可以让你免于这些;不过,这并不是真正的黑客行为,这只是事件队列的工作方式.此外,使用process.nextTick
实现这一目标要好得多,因为当您使用setTimeout
时,需要计算和判断经过的时间,而process.nextTick
只是我们真正想要的:"嘿,任务,回到队列的末尾,您已经使用了您的份额!"