Which would be better for concurrent tasks on node.js? Fibers? Web-workers? or Threads?

不久前我偶然发现了 node.js,我很喜欢它。但很快我就发现它严重缺乏执行 CPU 密集型任务的能力。因此,我开始在谷歌上搜索并得到了这些解决问题的答案: 纤维、网络工作者和线程(thread-a-gogo)。现在使用哪一个是一个困惑,其中一个肯定需要使用-毕竟是什么目的有一个服务器,只是在 IO 好,没有其他?需要建议!

更新:

I was thinking of a way off-late; just needing suggestions over it. Now, what I thought of was this: Let's have some threads (using thread_a_gogo or maybe webworkers). Now, when we need more of them, we can create more. But there will be some limit over the creation process. (not implied by the system but probably because of overhead). Now, when we exceed the limit, we can fork a new node, and start creating threads over it. This way, it can go on till we reach some limit (after all, processes too have a big overhead). When this limit is reached, we start queuing tasks. Whenever a thread becomes free, it will be assigned a new task. This way, it can go on smoothly.

我就是这么想的。这个主意好吗?我对所有这些过程和线程的东西都有点新,所以在这方面没有任何专业知识。请分享你的观点。

谢谢

42178 次浏览

我不确定 webworker 是否与本案有关,它们是客户端技术(在浏览器中运行) ,而 node.js 运行在服务器上。据我所知,光纤也是阻塞的,也就是说,它们是自愿的多任务处理,因此您可以使用它们,但是应该通过 yield自己管理上下文切换。线程可能正是您所需要的,但我不知道它们在 node.js 中有多成熟。

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

这并不是说 Node 是单线程的 有限公司。只是获得线程并发性的方法与您所寻找的方法不同。处理线程的标准方法是使用 Node 本身标准化的 集群模块。这是一种比在代码中手动处理线程更简单的方法。

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

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

也许更多关于你正在执行的任务的信息会有所帮助。为什么需要(正如您在对 genericdave 的回答的评论中所提到的)需要创建成千上万个它们?在 Node 中执行此类操作的通常方法是启动一个工作进程(使用 fork 或其他方法) ,该进程始终运行,并且可以通过消息与之进行通信。换句话说,不要在每次需要执行正在执行的任务时启动一个新的 worker,而是简单地向已经运行的 worker 发送一条消息,并在完成后获得响应。老实说,我不认为启动实际线程的 成千上万会非常有效,因为您仍然受到 CPU 的限制。

现在,在说了所有这些之后,我最近一直在使用 虎克做很多工作,它似乎对这种将任务卸载到其他进程的工作非常有效,也许它可以完成您需要的任务。

Node 有一个完全不同的范例,一旦正确地捕获了它,就更容易看到这种解决问题的不同方法。在 Node 应用程序(1)中,您永远不需要多个线程,因为您有不同的方法来做同样的事情。你可以创建多个进程,但是它与 Apache HTTP Server 的 Prefork mpm 有很大的不同。

现在,让我们假设我们只有一个 CPU 核心,我们将开发一个应用程序(以 Node 的方式)来完成一些工作。我们的工作是处理一个大文件,逐字节地运行其内容。对于我们的软件来说,最好的方法是从文件的开头开始工作,一字节一字节地跟踪到文件的结尾。

- 嘿,哈桑,我想你要么是一个新手或非常老学校从我祖父的时间! ! !为什么不创建一些线程,让它更快呢?

——哦,我们只有一个 CPU 核心。

- 那又怎样? 创造一些线程的人,使它更快!

不是这样的。如果我创建线程,我会让它变慢。因为我将为系统增加大量的线程间切换开销,试图给它们一定的时间,并且在我的进程中,试图在这些线程之间进行通信。除了所有这些事实之外,我还必须考虑如何将一个工作分成多个可以并行完成的部分。

——好吧,好吧,我看你很穷。让我们用我的电脑,它有32个核!

- 哇,你真棒,我亲爱的朋友,非常感谢。我很感激!

Then we turn back to work. Now we have 32 cpu cores thanks to our rich friend. Rules we have to abide have just changed. Now we want to utilize all this wealth we are given.

为了使用多个核心,我们需要找到一种方法来将我们的工作分成可以并行处理的部分。如果不是 Node,我们将使用线程; 32个线程,每个 CPU 核心一个线程。但是,因为我们有 Node,我们将创建32个 Node 进程。

线程可以是 Node 进程的一个很好的替代方法,甚至可能是一种更好的方法; 但是只有在已经定义了工作并且我们可以完全控制如何处理它的特定类型的作业中才是如此。除此之外,对于任务来自外部、我们无法控制且希望尽快回答的其他类型的问题,Node 的方法无疑更优越。

- 嘿,哈桑,你仍然工作单线程?你怎么回事,伙计?我刚给了你你想要的。你没有借口了。创建线程,让它运行得更快。

我已经把作品分成了几个部分,每个过程都会在其中一个部分上并行工作。

-- Why don't you create threads?

——对不起,我认为它不能用。你可以带走你的电脑,如果你想要的话?

- 不,好吧,我很酷,我只是不明白为什么你不使用线程?

——谢谢你的电脑。:)我已经把作品分成了几部分,并且创建了并行处理这些作品的流程。所有的 CPU 核心将被充分利用。我可以使用线程而不是进程来完成这项工作; 但 Node 有这种方式,我的老板 Parth Thakkar 希望我使用 Node。

-- Okay, let me know if you need another computer. :p

如果我创建33个进程,而不是32个,操作系统的调度程序将暂停一个线程,启动另一个线程,在一些周期后暂停它,再启动另一个线程... ... 这是不必要的开销。我不想要。事实上,在一个有32个核心的系统上,我甚至不想创建精确的32个进程,31个可以是 更好。因为在这个系统上运行的不仅仅是我的应用程序。给其他东西留一点空间也不错,特别是如果我们有32个房间的话。

我相信我们现在在充分利用 CPU 密集型任务处理器方面是一致的。

- 嗯,哈桑,我很抱歉有点嘲笑你。我相信我现在更了解你了。但是仍然有一些事情我需要解释: 运行数百个线程的所有嗡嗡声是什么?我到处都读到线程的创建速度比分叉进程快得多,也比分叉进程笨得多?您分叉进程而不是线程,并且您认为这是使用 Node 所能得到的最高值。那么 Node 不适合这种工作吗?

别担心,我也很酷。每个人都这么说,所以我想我已经习惯了。

——那么? Node 不适合这个?

—— Node 非常适合这种情况,尽管线程也可以很好。至于线程/进程的创建开销; 对于经常重复的事情,每一毫秒都很重要。然而,我只创建了32个进程,这只需要很少的时间。这只会发生一次。不会有什么区别的。

--那么,我什么时候要创建成千上万的线程呢?

——你永远不会想要创建成千上万的线程。但是,在一个从外部进行工作的系统上,比如 Web 服务器处理 HTTP 请求; 如果您为每个请求使用一个线程,那么您将创建大量线程,其中许多线程。

- 节点是不同的,虽然? 对不对?

是的,没错。这就是 Node 真正闪耀的地方。就像线程比进程轻很多一样,函数调用也比线程轻很多。节点调用函数,而不是创建线程。在 Web 服务器的示例中,每个传入请求都会导致一个函数调用。

——嗯,有意思; 但是如果不使用多个线程,那么只能同时运行一个函数。当许多请求同时到达 Web 服务器时,如何实现这一点呢?

--关于函数的运行方式,您是完全正确的,一次运行一个函数,而不是两个函数并行运行。我的意思是,在单个进程中,一次只运行一个代码作用域。操作系统调度程序不会来暂停这个函数并切换到另一个函数,除非它暂停进程以给予另一个进程时间,而不是我们的进程中的另一个线程。(2)

——那么一个进程如何能够同时处理两个请求?

--只要我们的系统有足够的资源(RAM、网络等) ,一个进程可以同时处理数万个请求。这些函数如何运行是关键的区别。

- 嗯,我现在应该兴奋吗?

-- Maybe :) Node runs a loop over a queue. In this queue are our jobs, i.e, the calls we started to process incoming requests. The most important point here is the way we design our functions to run. Instead of starting to process a request and making the caller wait until we finish the job, we quickly end our function after doing an acceptable amount of work. When we come to a point where we need to wait for another component to do some work and return us a value, instead of waiting for that, we simply finish our function adding the rest of work to the queue.

听起来太复杂了?

不,不,我可能听起来很复杂; 但是这个系统本身是非常简单的,而且非常有意义。

现在,我想停止引用这两个开发人员之间的对话,并在最后一个快速示例说明这些函数是如何工作的之后完成我的回答。

通过这种方式,我们正在做操作系统调度程序通常会做的事情。我们在某个时间点暂停工作,让其他函数调用(如多线程环境中的其他线程)运行,直到再次轮到我们。这比把工作留给操作系统调度器要好得多,因为后者试图给系统上的每个线程提供适当的时间。我们知道我们正在做什么比操作系统计划程序做得更好,我们希望停止时,我们应该停止。

Below is a simple example where we open a file and read it to do some work on the data.

同步方式:

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

我来自一个古老的思想学校,在那里我们使用多线程来使软件快速运行。在过去的3年里,我一直在使用 Node.js,并且是它的大支持者。Hasanyasin 详细解释了节点如何工作以及异步功能的概念。但让我在这里补充一些东西。

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

因此,喜欢或不信号线程或不多核心或单核心,你的进程将抢占和时间分割的操作系统。

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

因此,首先,它的所有在我们的软件设计。其次,不管他们告诉你什么,语境转换总是在发生。线程之所以存在并且仍然存在是有原因的,原因是它们在两个进程之间切换得更快。

在 node.js 的 Hood 下面是所有的 c + + 和线程。Node 提供了 c + + 的方式来扩展它的功能,并且在必须使用线程的地方使用线程来进一步提高速度,也就是说,阻塞任务,比如从一个源写入到一个源的读取,大型数据分析等等。

我知道 hasanyasin 的回答是可以接受的,但对我来说,线程将存在,无论你说什么或如何隐藏他们后面的脚本,其次,没有人只是为了速度把东西在线程它主要是为了阻塞任务。而且线程在 Node.js 的后台,所以在完全抨击多线程之前是正确的。另外,线程不同于进程,每个核心具有节点进程的限制并不完全适用于线程的数量,线程就像进程的子任务。事实上,线程不会出现在 Windows 任务管理器或 linux top 命令中。再一次,它们的重量比加工更小

(2016年更新: 网络工作者将进入 Io.js-a Node. js fork Node.js v7-见下文)

(Update 2017: Web workers are 没有 going into Node.js v7 or v8 - see below.)

(2018年更新: Web worker 进入 Node.js Node v10.5.0-见下文)

澄清一下

在阅读了上面的答案之后,我想指出的是,在 web worker 中,没有任何东西违背 JavaScript 的哲学,尤其是 Node 在并发性方面的哲学。(如果有的话,WHATWG 甚至不会讨论它,更不用说在浏览器中实现了)。

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

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

事实上,从概念上讲,web worker 非常适合 Node ——很多人都不知道——它很大程度上使用线程,而且事实上“除了代码,所有东西都是并行运行的”——参见:

但是 web worker 甚至不需要使用线程来实现。您可以在云中使用进程、绿色线程,甚至 RESTful 服务——只要使用 web worker API。通过值调用语义传递消息的 API 的完美之处在于,底层实现几乎是不相关的,因为并发模型的细节不会暴露出来。

单线程事件循环非常适合于 I/O 绑定操作。对于 CPU 绑定的操作,尤其是长时间运行的操作,它不能很好地工作。为此,我们需要产生更多的进程或使用线程。以可移植的方式管理子进程和行程间通讯可能相当困难,而且对于简单的任务来说,这通常被视为过度消耗,而使用线程意味着处理非常难以正确处理的锁和同步问题。

通常建议将长时间运行的 CPU 绑定操作划分为较小的任务(类似于 my answer to Speed up setInterval中“原始答案”部分的例子) ,但这并不总是实用的,并且不会使用多个 CPU 核。

我写这篇文章是为了澄清一些评论,这些评论基本上是说 web worker 是为浏览器而创建的,而不是服务器(忘记了在 JavaScript 中几乎所有东西都可以这么说)。

节点模块

几乎没有模块可以将 Web Workers 添加到 Node:

我没有使用过其中的任何一个,但是我有两个可能相关的快速观察: 截至2015年3月,node-webworker 最后一次更新是在4年前,node-webworker-thread 最后一次更新是在一个月前。另外,在 node-webworker-thread 使用的例子中,你可以使用函数而不是文件名作为 Worker 构造函数的参数,如果使用共享内存的线程来实现这个函数(除非这些函数只用于它的。ToString ()方法,并且在不同的环境中进行编译,在这种情况下,它可能没问题——我必须更深入地研究它,只是在这里分享我的观察结果)。

如果有任何其他相关项目在 Node 中实现了 Web 工作者 API,请留言。

更新1

在我写这篇文章的时候我还不知道,但是在我写这篇文章的前一天我偶然发现了这个问题。

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

它不仅证明了这样一个观点,即在网络工作者中没有任何东西违背 JavaScript 的一般哲学,尤其是 Node 在并发性方面的哲学,而且它可能会导致网络工作者成为像 io.js 这样的服务器端 JavaScript (可能在未来还有 Node.js)的一等公民,就像它已经在客户端 JavaScript 在所有现代浏览器中中一样。

更新2

在更新1和 我的推特中,我提到了 io.js pull 请求 # 1159 现在重定向到 节点 PR # 1159 7月8日关闭,取而代之的是仍然开放的 节点 PR # 2133。 在这些 pull 请求之下进行了一些讨论,这些请求可能会提供一些关于 io.js/Node.js 中 Web worker 状态的更新信息。

更新3

最新信息 -感谢 NiCk Newman 发布 评论: 这是佩特卡 · 安东诺夫从2015年9月6日开始的 workers: initial implementation承诺 可以下载并试用 此树 详情请参阅 评论: 尼克 · 纽曼

更新4

二零一六年五月为止,关于仍然开放的 PR # 2133-工人: 初步实施的最后评论已经有3个月了。5月30日,Matheus Moreira 要求我在下面的评论中更新这个回答,他在公关评论中发表了 要求这个功能的现状

在公关讨论中,最初的答案是怀疑的,但后来 Ben Noordhuis 写的说: “把这个合并成一个或另一个形状是我在 v7的待办事项清单”。

所有其他的评论似乎都支持这一观点,而且从2016年7月开始,在 Node 的下一个版本中应该可以使用 WebWorkers,7.0版似乎计划在 2016年10月上发布(不一定是以这个公关的形式)。

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

更新5

至于 二零一六年七月有几个模块在 npm 之前是不可用的-一个完整的相关模块列表,搜索 Npm的工作人员,网络工作人员,等等。如果有什么特别适合或不适合你,请发表评论。

更新6

2017年1月开始,网络工作者不太可能被并入 Node.js。

Petka Antonov 于2015年7月8日提出的 # 2133 工作人员: 初步实施请求最终被 Ben Noordhuis 于2016年12月11日提出,“多线程支持增加了太多新的失败模式,但收益不足”,“我们也可以使用更传统的方式,如共享内存和更高效的序列化来实现这一点。”

有关更多信息,请参见对 GitHub 上 PR 2133的注释。

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

Update 6

我很高兴地宣布,几天前,在 二零一八年六月网络工作者出现在 Nodev10.5.0作为一个实验性功能与 --experimental-worker标志激活。

有关更多信息,请参见:

终于来了!我可以对我3年前的 Stack Overflow 回答做第7次更新,在那里我认为线程一个 la web worker 并不违背 Node 哲学,只是这次说我们终于得到了它!

worker_threads已经实现,并在 node@10.5.0中以标志的形式发布。它仍然是一个初始的实现,需要更多的努力来使它在未来的版本中更加有效。最新的 node值得一试。