对于node.js上的并发任务哪个更好?纤维?网络工作人员?还是线程?

Par*_*kar 109 concurrency multithreading web-worker fibers node.js

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

更新:

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

所以,这就是我的想法.这个想法好吗?我对所有这些过程和线程都有点新意,所以没有任何专业知识.请分享您的意见.

谢谢.:)

has*_*sin 328

Node具有完全不同的范例,一旦正确捕获,就更容易看到解决问题的这种不同方式.您永远不需要Node应用程序中的多个线程(1),因为您有不同的方式来执行相同的操作.您创建了多个流程; 但它与Apache Web Server的Prefork mpm的功能非常不同.

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

- 嘿,哈桑,我想你要么是我祖父的新手还是非常老派!为什么不创建一些线程并使其更快?

- 哦,我们只有一个CPU核心.

- 所以呢?创建一些线程man,让它更快!

- 它不像那样工作.如果我创建线程,我将使它变慢.因为我将在系统中添加大量开销以在线程之间切换,试图给它们一定的时间,并在我的进程内尝试在这些线程之间进行通信.除了所有这些事实之外,我还必须考虑如何将单个作业分成多个可以并行完成的作业.

- 好的,我觉得你很穷.我们用我的电脑,它有32个核心!

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

然后我们回头工作.现在我们拥有32个cpu核心,感谢我们的朋友.我们必须遵守的规则刚刚改变了.现在我们想要利用我们给予的所有这些财富.

要使用多个内核,我们需要找到一种方法将我们的工作划分为可以并行处理的部分.如果它不是Node,我们会为此使用线程; 32个线程,每个cpu核心一个.但是,由于我们有Node,我们将创建32个Node进程.

线程可以是Node流程的一个很好的替代方案,甚至可能是更好的方法; 但只有在已定义工作的特定工作中,我们才能完全控制如何处理工作.除此之外,对于其他任何类型的问题,如果工作来自外部,我们无法控制,我们希望尽快回答,Node的方式无可争议地优越.

- 嘿,哈桑,你还在单线程吗?你有什么问题,伙计?我刚刚为你提供了你想要的东西.你没有任何借口了.创建线程,让它运行得更快.

- 我已将作品分成几部分,每个过程都会同时处理其中一个部分.

- 你为什么不创建线程?

- 对不起,我觉得它不可用.你可以随身携带电脑吗?

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

- 谢谢你的电脑.:)我已经将工作分成几部分,我创建了并行处理这些部分的流程.所有CPU核心都将得到充分利用.我可以用线程而不是进程来做到这一点; 但Node有这种方式,我的老板Parth Thakkar希望我使用Node.

- 好的,如果你需要另一台电脑,请告诉我.:p

如果我创建了33个进程而不是32个进程,那么操作系统的调度程序将暂停一个线程,启动另一个进程,在一些循环后暂停它,再次启动另一个进程......这是不必要的开销.我不想要这个.实际上,在具有32个内核的系统上,我甚至不想创建正好32个进程,31个可以更好.因为不仅仅是我的应用程序可以在这个系统上工作.为其他东西留一点空间可能会很好,特别是如果我们有32个房间.

我相信我们现在在同一页上关于充分利用处理器来执行CPU密集型任务.

- 嗯,哈桑,我很抱歉嘲笑你一点.我相信我现在更了解你.但是我还需要解释一下:运行数百个线程的嗡嗡声是什么?我到处都读到线程比分叉进程更快创建和愚蠢?你分叉进程而不是线程,你认为它是你用Node获得的最高分.然后Node不适合这种工作吗?

- 不用担心,我也很酷.每个人都说这些东西,所以我觉得我习惯听到它们.

- 那么?节点对此不好?

- 即使线程也很好,节点也非常适合这种情况.至于线程/进程创建开销; 对于你重复很多的事情,每一毫秒都很重要.但是,我只创建了32个进程,这将花费很少的时间.它只会发生一次.它没有任何区别.

- 我什么时候想创建数千个线程呢?

- 你永远不想创建成千上万的线程.但是,在正在进行外部工作的系统上,例如处理HTTP请求的Web服务器; 如果你为每个请求使用一个线程,你将创建很多线程,其中许多线程.

- 节点有所不同吗?对?

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

- 嗯,有趣; 但是如果不使用多个线程,则只能同时运行一个函数.当许多请求同时到达Web服务器时,它如何工作?

- 关于函数如何运行,一次一个,从不并行运行,你是完全正确的.我的意思是在一个进程中,一次只运行一个代码范围.操作系统调度程序不会暂停此功能并切换到另一个功能,除非它暂停进程以给其他进程留出时间,而不是我们进程中的另一个进程.(2)

- 那么一个进程如何一次处理2个请求?

- 只要我们的系统有足够的资源(RAM,网络等),一个进程就可以同时处理数万个请求.这些功能如何运行是关键的差异.

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

- 也许:)节点在队列上运行一个循环.在这个队列中是我们的工作,即我们开始处理传入请求的调用.这里最重要的一点是我们设计运行函数的方式.我们不是开始处理请求并让调用者等到我们完成工作,而是在完成可接受的工作量后快速结束我们的功能.当我们需要等待另一个组件做一些工作并返回一个值而不是等待它时,我们只需完成我们的功能,将剩余的工作添加到队列中.

- 听起来太复杂了?

- 不,不,我可能听起来很复杂; 但系统本身非常简单,而且非常有意义.

现在我想停止引用这两个开发人员之间的对话,并在最后一个关于这些功能如何工作的快速示例之后完成我的回答.

通过这种方式,我们正在做OS Scheduler通常会做的事情.我们暂停我们的工作,并让其他函数调用(如多线程环境中的其他线程)运行,直到我们再次轮到我们.这比将工作留给OS Scheduler好得多,后者试图给系统上的每个线程提供时间.我们知道我们在做什么比OS Scheduler做得更好,我们应该在我们停止时停止.

下面是一个简单的例子,我们打开一个文件并阅读它来对数据做一些工作.

同步方式:

Open File
Repeat This:    
    Read Some
    Do the work
Run Code Online (Sandbox Code Playgroud)

异步方式:

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
Run Code Online (Sandbox Code Playgroud)

如您所见,我们的函数要求系统打开一个文件,而不是等待它打开.文件准备好后,通过提供后续步骤完成自己.当我们返回时,Node在队列上运行其他函数调用.在遍历所有函数后,事件循环移动到下一轮...

总之,Node具有与多线程开发完全不同的范例; 但这并不意味着它缺乏东西.对于同步作业(我们可以决定处理的顺序和方式),它可以和多线程并行一样工作.对于来自外部的工作,如对服务器的请求,它只是优越的.


(1)除非您使用其他语言(如C/C++)构建库,否则您仍然不会创建用于分割作业的线程.对于这种工作,你有两个线程,其中一个将继续与Node通信,而另一个完成真正的工作.

(2)事实上,每个Node进程都有多个线程,原因与我在第一个脚注中提到的相同.然而,这并不像1000个线程做类似的工作.这些额外的线程用于接受IO事件和处理进程间消息传递.

更新(回复评论中的好问题)

@Mark,谢谢你的建设性批评.在Node的范例中,除非队列中的所有其他调用都设计为一个接一个地运行,否则您应该永远不会有需要太长时间才能处理的函数.在计算成本高昂的任务的情况下,如果我们完整地看图片,我们会发现这不是"我们应该使用线程还是进程?"的问题.但是问题是"我们怎样才能将这些任务以均衡的方式划分为子任务,我们可以在系统中并行使用多个CPU核心来运行它们?" 假设我们将在具有8个内核的系统上处理400个视频文件.如果我们想一次处理一个文件,那么我们需要一个处理同一文件不同部分的系统,在这种情况下,多线程单进程系统可能更容易构建,甚至更高效.我们仍然可以通过运行多个进程并在需要进行状态共享/通信时在它们之间传递消息来使用Node.正如我之前所说,使用Node的多进程方法以及这种任务中的多线程方法; 但不仅仅是那个.再次,正如我之前所说,Node发光的情况是当我们将这些任务作为来自多个源的系统输入时,因为与每个连接的线程或每个连接的进程相比,节点中的多个连接同时要轻得多系统.

至于setTimeout(...,0)电话; 有时在耗时的任务中休息以允许队列中的呼叫可能需要它们的处理份额.以不同方式划分任务可以使您免于这些; 但是,这不是真正的黑客攻击,它只是事件队列工作的方式.此外,使用process.nextTick这个目标要好得多,因为当你使用时setTimeout,计算和检查所需的时间是必要的,而process.nextTick这正是我们真正想要的:"嘿任务,回到队列的末尾,你已经使用了你的分享! "

  • 当然:)我真的不敢相信有非常卑鄙的人在那里投票这个答案文章!提问者称之为"该死的惊人!" 一本书的作者在看到这篇文章之后就给我写了他的网站; 但是有些天才在那里投票.你为什么不分享你明智的知识分子质量并对其进行评论而不是吝啬和狡猾的投票,对吧?为什么这么好的东西会让你感到不安?你为什么要阻止一些有用的东西来吸引那些真正从中受益的人呢? (48认同)
  • 惊人!该死的太神奇了!我喜欢你回答这个问题的方式!:) (9认同)
  • 这不是一个完全公平的答案.那些计算成本高昂的任务,我们不能"快速结束"我们的函数调用呢?我相信有些人会为此使用一些`setTimeout(...,0)`hacks,但在这种情况下使用单独的线程肯定会更好? (9认同)
  • @Mark一般来说,如果它的计算成本很高,那么对于胎面/过程工作者来说有选项/模块......通常对于这些类型的东西,我使用消息队列,并且具有处理任务的工作进程.从队列中抽出时间,并完成该任务.这也允许扩展到多个服务器.沿着这些方向,Substack有许多模块可用于配置和扩展,您可以查看. (7认同)
  • @hasanyasin这是我到目前为止找到的节点上最好的解释!:) (3认同)
  • 确实是一个惊人的答案,但我不认为它真的回答了这个问题.你最接近的是你对@Mark的回答,但它确实显示了Node.js的局限性:为了实现并发,你需要通过消息传递将这项工作卸载到C++或外部服务.顺便说一句,您希望并发不仅适用于CPU绑定工作,还适用于维护数据库的连接池.在Node.js中,这确实是由数据库驱动程序(C++)处理的.最重要的是Node.js不是并发需求的最佳工具......除非你愿意为了繁重的工作而"下载"到C++. (2认同)
  • 在我看来,OP和这个问题的选择回答者是关于SO社区应该如何运作的***完美***例子 - 建设性问题,*非常*建设性的答案,以及编程的人性方面很高兴看到我们的职业.如果我可以提出这个问题并回答10000次,我会...每个人都应该看到它.我甚至没有理由尝试多线程,但我仍然喜欢阅读这个**咳嗽***线程*;) (2认同)

rsp*_*rsp 33

(2016年更新:Web工作者将进入io.js - 一个Node.js派生 Node.js v7 - 见下文.)

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

(更新2018:网络工作者进入Node.js的节点V10.5.0 -见下文).

一些澄清

阅读完上面的答案后,我想指出的是,Web工作者中没有任何内容违背JavaScript的哲学,特别是Node的并发性.(如果有的话,它甚至不会被WHATWG讨论,更不用说在浏览器中实现了).

您可以将Web工作者视为异步访问的轻量级微服务.没有共享任何州.没有锁定问题.没有阻止.无需同步.就像当您使用Node程序中的RESTful服务时,您不必担心它现在是"多线程"的,因为RESTful服务与您自己的事件循环不在同一个线程中.它只是一个单独的服务,您可以异步访问,这是重要的.

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

事实上,Web工作者在概念上非常适合Node - 正如许多人所不知道的那样 - 偶然使用了线程,事实上"除了代码之外,所有内容都是并行运行" - 请参阅:

但是Web工作者甚至不需要使用线程来实现.只要使用Web worker API,您就可以在云中使用进程,绿色线程甚至RESTful服务.通过值语义调用传递API的消息的整体美妙之处在于底层实现几乎无关紧要,因为并发模型的细节不会暴露.

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

通常建议将长时间运行的CPU绑定操作划分为较小的任务(类似于我对Speed up setInterval的回答的"原始答案"部分中的示例),但它并不总是实用且不使用更多比一个CPU核心.

我写这篇文章是为了澄清那些基本上是说网络工作者是为浏览器而不是服务器创建的评论(忘记了几乎所有的JavaScript都可以说).

节点模块

应该将Web Workers添加到节点的模块很少:

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

如果在Node中有任何其他相关项目实现Web worker API,请发表评论.

更新1

我不知道它在写作的时间,但偶然的前一天,我写这个答案的Web工人加入io.js.

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

它不仅证明了网络工作者中没有任何东西违背JavaScript的哲学,特别是Node的并发性,而且可能导致Web工作者成为像io这样的服务器端JavaScript的一等公民. js(以及未来可能的Node.js)就像它在所有现代浏览器中的客户端JavaScript 中一样.

更新2

在Update 1和我的推文中,我指的是io.js pull请求#1159 ,它现在重定向到 7月8日关闭的节点PR#1159,并替换为仍然打开的节点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评论中询问了这个功能的当前状态.

公关讨论中的第一个答案是持怀疑态度,但后来Ben Noordhuis 写道:"将这种形式合并为一种形式在我的待办事项列表中为v7".

所有其他评论似乎排在第二位,截至2016年7月,似乎Web Workers应该在下一版本的Node 7.0 版本中可用,该版本计划于2016年10月发布(不一定以这种确切的PR形式发布).

感谢Matheus Moreira在评论中指出并重新开始讨论GitHub.

更新5

截至2016年7月,npm上几乎没有以前无法使用的模块 - 有关相关模块的完整列表,搜索工作人员,网络工作者等的npm.如果有任何特别对您有效或无效,请发布一个评论.

更新6

截至2017年1月,Web工作者不太可能合并到Node.js.

牵引请求#2133 工人: Petka Antonov从2015年7月8日开始实施最终由Ben Noordhuis于2016年12月11日关闭,他评论说"多线程支持增加了太多新的故障模式,没有足够的利益"和"我们也可以使用更传统的方式,如共享内存和更高效的序列化来实现这一目标."

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

再次感谢Matheus Moreira在评论中指出它.

更新6

我很高兴地宣布,几天前,在20186月,网络工作者出现在Node v10.5.0中,作为用--experimental-worker旗帜激活的实验性功能.

有关详细信息,请参阅:

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


小智 8

我来自古老的思想流派,我们使用多线程来快速制作软件.在过去的3年里,我一直在使用Node.js和它的支持者.正如hasanyasin详细解释了节点如何工作以及异常功能的概念.但是,我在这里添加一些东西.

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

所以喜欢或不喜欢信号线程或不是多核或单核,你的进程将被操作系统抢占和时间切片.

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

所以首先是我们软件的设计.其次,无论他们告诉你什么,上下文切换总是会发生.线程在那里并且仍然存在是有原因的,原因是它们在然后在进程之间切换更快.

在node.js的引擎下它的所有c ++和线程.节点提供c ++方式来扩展其功能,并通过使用线程来进一步加速,即阻塞任务,例如从源写入源读取,大数据分析等等.

我知道hasanyasin的答案是被接受的答案,但对我而言,无论你说什么或者你如何将它们隐藏在脚本后面,线程都会存在,其次没有人只是为了速度而破坏线程,它主要用于阻塞任务.并且线程位于Node.js的后端,因此在完全打击多线程之前是正确的.线程也与进程不同,每个核心拥有节点进程的限制并不完全适用于线程数,线程就像进程的子任务.实际上线程赢了; t出现在你的windows任务管理器或linux top命令中.再一次,它们的重量就更小了