113

前段时间我偶然发现了 node.js 并且非常喜欢它。但很快我发现它严重缺乏执行 CPU 密集型任务的能力。所以,我开始在谷歌上搜索并得到这些答案来解决这个问题:Fibers, Webworkers and Threads (thread-a-gogo)。现在使用哪一个是一个混乱,其中一个肯定需要使用 - 毕竟拥有一个只擅长 IO 而没有别的服务器的目的是什么?需要的建议!

更新:

我在想办法迟到;只是需要建议。现在,我想到的是:让我们有一些线程(使用 thread_a_gogo 或 webworkers)。现在,当我们需要更多它们时,我们可以创造更多。但是创建过程会有一些限制。(系统没有暗示,但可能是因为开销)。现在,当我们超过限制时,我们可以分叉一个新节点,并开始在其上创建线程。这样,它可以一直持续到我们达到某个限制(毕竟,进程也有很大的开销)。当达到这个限制时,我们开始排队任务。每当一个线程空闲时,它就会被分配一个新任务。这样,它就可以顺利进行。

所以,我就是这么想的。这个主意好吗?我对所有这些进程和线程的东西有点陌生,所以没有任何专业知识。请分享您的意见。

谢谢。:)

4

7 回答 7

332

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这正是我们真正想要的:“嘿任务,回到队列末尾,您已经使用了您的份额! "

于 2012-07-01T00:25:47.423 回答
35

(2016 年更新:Web 工作者正在进入io.js - 一个 Node.js 分支Node.js v7 - 见下文。)

(2017 年更新:Web 工作者不会进入 Node.js v7 或 v8 - 见下文。)

(2018 年更新:Web 工作者进入 Node.js Node v10.5.0 - 见下文。)

一些澄清

阅读了上面的答案后,我想指出,Web Worker 中没有任何内容违反 JavaScript 的一般哲学,特别是关于并发性的 Node。(如果有,WHATWG 甚至都不会讨论它,更不用说在浏览器中实现了)。

您可以将 Web Worker 视为异步访问的轻量级微服务。没有状态是共享的。不存在锁定问题。没有阻塞。不需要同步。就像您在 Node 程序中使用 RESTful 服务时一样,您不必担心它现在是“多线程的”,因为 RESTful 服务与您自己的事件循环不在同一个线程中。它只是一个单独的服务,您可以异步访问,这才是最重要的。

网络工作者也是如此。它只是一个与运行在完全独立的上下文中的代码进行通信的 API,它是否在不同的线程、不同的进程、不同的 cgroup、区域、容器或不同的机器中完全不相关,因为它是一个严格异步、非阻塞的 API,所有数据按值传递。

事实上,从概念上讲,Web Worker 非常适合 Node,正如许多人所不知道的那样,它偶尔会大量使用线程,实际上“除了你的代码之外,所有东西都是并行运行的”——请参阅:

但是网络工作者甚至不需要使用线程来实现。您可以在云中使用进程、绿色线程甚至 RESTful 服务——只要使用 Web Worker API。具有按值调用语义的消息传递 API 的全部优点在于底层实现几乎无关紧要,因为不会暴露并发模型的细节。

单线程事件循环非常适合 I/O 绑定操作。它不适用于 CPU 密集型操作,尤其是长时间运行的操作。为此,我们需要生成更多进程或使用线程。以可移植的方式管理子进程和进程间通信可能非常困难,并且通常被视为简单任务的过度杀伤力,而使用线程意味着处理很难正确处理的锁和同步问题。

通常建议将长时间运行的 CPU 密集型操作划分为较小的任务(类似于我对 Speed up setInterval 的回答的“原始答案”部分中的示例),但这并不总是实用的,而且它不会使用更多超过一个 CPU 核心。

我写它是为了澄清那些基本上说网络工作者是为浏览器而不是服务器创建的评论(忘记了它可以说几乎所有 JavaScript 中的东西)。

节点模块

有几个模块应该将 Web Worker 添加到 Node:

我没有使用过它们中的任何一个,但我有两个可能相关的快速观察结果:截至 2015 年 3 月,node-webworker 最后一次更新是在 4 年前,node-webworker-threads 最后一次更新是在一个月前。此外,我在 node-webworker-threads 使用示例中看到,您可以使用函数而不是文件名作为 Worker 构造函数的参数,如果使用共享内存的线程实现,这似乎可能会导致微妙的问题(除非函数仅用于其 .toString() 方法,否则会在不同的环境中编译,在这种情况下可能没问题 - 我必须更深入地研究它,只是在这里分享我的观察)。

如果有其他相关项目在 Node 中实现了 web workers API,请发表评论。

更新 1

在撰写本文时我还不知道,但顺便说一下,在我写这个答案的前一天,Web Workers 被添加到 io.js

io.js是 Node.js 的一个分支 - 请参阅:为什么 io.js 决定分支 Node.js,InfoWorld 对 Mikeal Rogers 的采访,了解更多信息。)

它不仅证明了 web worker 中没有任何东西违反 JavaScript 的哲学,特别是 Node 在并发方面的哲学,而且它可能导致 web worker 成为像 io 一样的服务器端 JavaScript 中的一等公民。 Node.js(未来可能还有 Node.js),就像它已经在所有现代浏览器的客户端 JavaScript 中一样。

更新 2

在更新 1 和我的推文中,我指的是io.js 拉取请求 #1159 ,它现在重定向到 节点 PR #1159 ,该节点于 7 月 8 日关闭,并替换为节点 PR #2133 - 它仍然处于打开状态。在这些拉取请求下进行了一些讨论,可能会在 io.js/Node.js 中提供有关 Web 工作者状态的更多最新信息。

更新 3

最新信息- 感谢 NiCk Newman 在评论中发布它:有工人:Petka Antonov 从 2015 年 9 月 6 日开始的初始实施提交,可以在此树中下载和试用 。有关详细信息,请参阅NiCk Newman的评论。

更新 4

截至2016 年 5 月,关于仍然开放的PR #2133的最后评论- 工人:初始实施已经 3 个月大。5 月 30 日,Matheus Moreira 要求我在下面的评论中发布对此答案的更新,并在 PR 评论中询问此功能的当前状态。

PR 讨论中的第一个答案是持怀疑态度的,但后来 Ben Noordhuis写道:“将其合并为一种或另一种形式在我的 v7 待办事项清单上”。

所有其他评论似乎都支持这一点,截至 2016 年 7 月,似乎Web Worker 应该在计划于2016 年 10 月发布的下一版本 Node 7.0 中可用(不一定以这个确切 PR 的形式)。

感谢 Matheus Moreira 在评论中指出这一点并在 GitHub 上恢复讨论。

更新 5

截至2016 年 7 月,npm 上几乎没有以前不可用的模块 - 有关相关模块的完整列表,请在npm中搜索worker、web workers 等。如果有什么特别适合您或不适合您,请发布评论。

更新 6

截至2017 年 1 月,Web Worker 不太可能合并到 Node.js 中。

拉取请求 #2133 workers: Petka Antonov 从 2015 年 7 月 8 日开始的初始实施最终被 Ben Noordhuis 于 2016 年 12 月 11 日关闭,他评论说“多线程支持增加了太多新的故障模式而没有足够的好处”和“我们还可以使用更传统的方式来实现这一点,例如共享内存和更有效的序列化。”

有关更多信息,请参阅GitHub 上对PR 2133的评论。

再次感谢 Matheus Moreira 在评论中指出这一点。

更新 6

我很高兴地宣布,几天前,2018 年 6 月, Web Worker 出现在 Node v10.5.0 中,作为使用--experimental-worker标志激活的实验性功能。

有关详细信息,请参阅:

最后!我可以对我 3 岁的 Stack Overflow 答案进行第 7 次更新,我认为线程化网络工作者并不违反 Node 哲学,只是这一次说我们终于明白了!

于 2015-03-16T22:38:21.503 回答
8

我来自旧的思想流派,我们使用多线程来快速开发软件。在过去的 3 年里,我一直在使用 Node.js,并且是它的大力支持者。正如 hasanyasin 详细解释的节点如何工作以及异步功能的概念。但是,让我在这里添加一些东西。

回到过去,单核和较低的时钟速度我们尝试了各种方法来使软件快速并行工作。在 DOS 时代,我们习惯一次运行一个程序。比在 Windows 中我们开始一起运行多个应用程序(进程)。诸如抢占式和非抢占式(或合作式)之类的概念已经过测试。我们现在知道抢占式是在单核计算机上实现更好的多处理任务的答案。随之而来的是流程/任务和上下文切换的概念。比线程的概念进一步减轻了进程上下文切换的负担。线程被创造为产生新进程的轻量级替代品。

因此,无论喜欢与否,信号线程或不是多核或单核,您的进程都将被操作系统抢占和时间切片。

Nodejs 是一个单一的进程并提供异步机制。在这里,作业被分派到底层操作系统以执行任务,同时我们在事件循环中等待任务完成。一旦我们从操作系统获得绿色信号,我们就会执行我们需要做的任何事情。现在从某种意义上说,这是协作/非抢占式多任务,所以我们永远不应该长时间阻塞事件循环,否则我们会很快降低我们的应用程序。
因此,如果有一个任务本质上是阻塞的或非常耗时的,我们将不得不将其分支到操作系统和线程的抢占式世界。libuv 文档中有很好的例子。此外,如果您进一步阅读文档,您会发现FileI/O 是在 node.js 的线程中处理的

因此,首先这一切都在我们的软件设计中。其次,无论他们告诉你什么,上下文切换总是会发生。线程存在并且仍然存在是有原因的,原因是它们在进程之间切换的速度更快。

在 node.js 的引擎盖下,它的所有 c++ 和线程。node 提供了 c++ 方法来扩展其功能,并通过在必须使用线程的情况下进一步加快速度,即阻塞任务,例如从源读取写入源、大数据分析等。

我知道 hasanyasin 的答案是公认的答案,但对我来说,无论你说什么或如何将它们隐藏在脚本后面,线程都会存在,其次,没有人只是为了速度而将事情分成线程,它主要是为了阻塞任务而完成的。线程位于 Node.js 的骨干中,因此在完全抨击多线程之前是正确的。此外,线程与进程不同,每个核心拥有节点进程的限制并不完全适用于线程数,线程就像进程的子任务。实际上线程不会出现在您的 Windows 任务管理器或 linux top 命令中。再一次,它们比过程更轻

于 2015-01-27T07:10:13.203 回答
4

我不确定 webworkers 在这种情况下是否相关,它们是客户端技术(在浏览器中运行),而 node.js 在服务器上运行。据我所知,纤维也是阻塞的,即它们是自愿的多任务处理,所以你可以使用它们,但应该通过自己管理上下文切换yield。线程可能实际上是您需要的,但我不知道它们在 node.js 中有多成熟。

于 2012-05-27T11:21:39.877 回答
3

worker_threads已在node@10.5.0. 它仍然是一个初步的实现,需要更多的努力来使其在未来的版本中更有效。最近值得一试node

于 2018-06-23T09:27:04.630 回答
2

在许多 Node 开发人员看来,Node 最好的部分之一实际上是它的单线程特性。线程给共享资源带来了一大堆困难,Node 通过非阻塞 IO 完全避免了这些困难。

这并不是说 Node仅限于单个线程。只是获取线程并发的方法和你要找的不一样。处理线程的标准方法是使用 Node 本身标配的集群模块。这是一种比在代码中手动处理线程更简单的方法。

为了在代码中处理异步编程(例如,避免嵌套回调金字塔),Fibers库中的 [Future] 组件是一个不错的选择。我还建议您查看基于 Fibers 的Asyncblock 。纤维很好,因为它们允许您通过复制堆栈然后在需要时在单线程上的堆栈之间跳转来隐藏回调。为您省去真实线程的麻烦,同时为您带来好处。缺点是使用 Fibers 时堆栈跟踪可能会有点奇怪,但它们并不算太糟糕。

如果您不需要担心异步的东西,而只是对在不阻塞的情况下进行大量处理感兴趣,那么您只需要每隔一段时间简单地调用一次 process.nextTick(callback) 即可。

于 2012-05-27T17:03:59.873 回答
1

也许有关您正在执行的任务的更多信息会有所帮助。为什么您需要(正如您在对genericdave 回答的评论中提到的那样)需要创建成千上万个?在 Node 中做这类事情的常用方法是启动一个工作进程(使用 fork 或其他方法),该进程始终运行并且可以使用消息进行通信。换句话说,不要在每次需要执行正在执行的任何任务时都启动新的工作人员,而只需向已经运行的工作人员发送消息并在完成时得到响应。老实说,我看不出启动数千个实际线程会非常有效,你仍然受到 CPU 的限制。

现在,说了这么多之后,我最近一直在用Hook.io做很多工作,这似乎非常适合这种将任务卸载到其他进程中,也许它可以完成你所需要的。

于 2012-06-30T09:36:02.677 回答