Async.js – 并行真的平行吗?

正如我迄今为止所理解的:Javascript是单线程的。 如果推迟某个过程的执行,则只需在该线程空闲时安排它(排队)以便运行。 但Async.js定义了两个方法: Async::parallel & Async::parallelLimit和我引用:

  • 并行(任务,[callback])

并行运行一组函数,而不必等到上一个函数完成。 如果任何函数将错误传递给它的callback函数

  • parallelLimit(任务,限制,[callback])

与并行相同,只有在任何时候执行的最大“限制”任务才能并行执行任务。

就我对英语的理解而言,当你说:“并行执行任务”是指同时执行这些任务。

Async.js如何在单个线程中并行执行任务? 我错过了什么

Async.js如何在单个线程中并行执行任务? 我错过了什么

parallel运行所有的任务。 所以,如果你的任务包含I / O调用(例如查询数据库),他们将看起来好像他们已经被并行处理。

这是如何启用在一个单一的线程? 这是我无法理解的。

Node.js是非阻塞的。 所以不是并行处理所有任务,而是从一个任务切换到另一个任务。 所以当第一个任务使得I / O调用自己空闲的时候,Node.js只是简单的切换到另外一个。

I / O任务花费大部分处理时间等待I / O调用的结果。 在诸如Java之类的阻塞语言中,这样的任务在等待结果时阻塞其线程。 但Node.js利用它的时间来处理另一个任务,而不是等待。

所以这意味着如果每个任务的内部处理都是asynchronous的,那么不pipe这些任务中的任何一个是否完成,直到所有的任务都完成了,这个线程被授予这个任务的每一个比特?

是的,这几乎和你所说的一样。 Node.js开始处理第一个任务,直到它暂停执行I / O调用。 此时,Node.js离开它,并将其主线程授予另一个任务。 所以你可以说这个线程依次被授予了每个活动任务。

这些函数不是同时执行的,但是当第一个函数切换到asynchronous任务(例如setTimeout,network,…)时,即使第一个函数没有调用提供的callback,第二个函数也会启动。

至于并行任务的数量:这取决于你select什么。

Async.Parallel在这里有很好的文档: https : //github.com/caolan/async#parallel

Async.Parallel是关于并行启动I / O任务,而不是并行执行的代码。 如果您的任务不使用任何定时器或执行任何I / O,那么它们将实际上被串行执行。 每个任务的任何同步设置部分都会一个接一个地发生。 JavaScript仍然是单线程的。

就我对英语的理解而言,当你说:“并行执行任务”是指同时执行这些任务。

正确。 而“同时”是指“至less有一个以上的任务已经开始,但尚未完成”。

Async.js如何在单个线程中并行执行任务? 我错过了什么

当某些任务由于某种原因(即IO)而停止时,async.js将执行另一个任务并稍后继续。

你的疑惑很有道理 你问这个问题已经有几年了,但是我认为值得给已有的答案增加几个想法。

并行运行一组函数,而不必等到上一个函数完成。 如果任何函数将错误传递给它的callback函数

这句话不完全正确。 实际上它等待每个函数都完成,因为在JavaScript中不可能不这样做。 函数调用和函数返回都是同步和阻塞的。 所以当它调用任何函数时,都必须等待它返回。 它不需要等待的是传递给该函数的callback的调用。

寓言

前段时间我写了一个小故事来展示这个概念:

  • 在这个星球上的非I / Oasynchronous256/16

引用它的一部分:

“所以我说:'等一下,你告诉我,一个蛋糕需要三个半小时,四个蛋糕只有一个半小时多一个? 这没有任何意义! 尽pipe如此,她一定是在开玩笑,所以我开始笑了起来。“
“但她不是在开玩笑吗?
“不,她看着我说:”这很有道理。 这一次大部分是等待。 我可以一下子等很多事情。 我停止了笑,开始思考。 它终于开始接近我。 在同一时间做四个枕头没有任何时间买你,也许它可以说是更容易组织,但后来可能不会。 但是这一次是不同的。 但我真的不知道如何使用这些知识。“

理论

我认为重要的是要强调,在单线程事件循环中,你永远不能同时做多个事情。 但是你可以一下子等待很多事情。 这就是这里发生的事情。

来自Async模块的并行函数逐个调用每个函数,但是每个函数在下一个函数被调用之前必须返回,这是没有办法的。 这里的奇妙之处在于函数在返回之前并没有真正做到它的工作 – 它只是调度一些任务,注册一个事件监听器,在其他地方传递一些callback,为某个promise添加一个parsing处理器等等。

然后,当计划的任务完成时,会执行一些之前由该函数注册的处理程序,然后执行最初由Async模块传递的callback,并且Async模块知道这个函数已经完成 – 这一次不仅在某种意义上说,它返回,而且传递给它的callback最终被调用。

例子

所以,举个例子,假设你有3个函数可以下载3个不同的URL: getA()getB()getC()

我们将写一个模拟的请求模块来模拟请求和一些延迟:

 function mockRequest(url, cb) { const delays = { A: 4000, B: 2000, C: 1000 }; setTimeout(() => { cb(null, {}, 'Response ' + url); }, delays[url]); }; 

现在三个function大部分是相同的,详细的日志logging:

 function getA(cb) { console.log('getA called'); const url = 'A'; console.log('getA runs request'); mockRequest(url, (err, res, body) => { console.log('getA calling callback'); cb(err, body); }); console.log('getA request returned'); console.log('getA returns'); } function getB(cb) { console.log('getB called'); const url = 'B'; console.log('getB runs request'); mockRequest(url, (err, res, body) => { console.log('getB calling callback'); cb(err, body); }); console.log('getB request returned'); console.log('getB returns'); } function getC(cb) { console.log('getC called'); const url = 'C'; console.log('getC runs request'); mockRequest(url, (err, res, body) => { console.log('getC calling callback'); cb(err, body); }); console.log('getC request returned'); console.log('getC returns'); } 

最后我们用async.parallel函数调用它们:

 async.parallel([getA, getB, getC], (err, results) => { console.log('async.parallel callback called'); if (err) { console.log('async.parallel error:', err); } else { console.log('async.parallel results:', JSON.stringify(results)); } }); 

立即显示的是这样的:

 getA called getA runs request getA request returned getA returns getB called getB runs request getB request returned getB returns getC called getC runs request getC request returned getC returns 

正如你所看到的,这是所有的顺序 – 函数被逐个调用,下一个在前一个函数返回之前不被调用。 然后我们看到这个有些延误:

 getC calling callback getB calling callback getA calling callback async.parallel callback called async.parallel results: ["Response A","Response B","Response C"] 

所以getC首先完成,然后getBgetC – 然后只要最后一个完成, async.parallel调用我们的callback,所有的响应组合,并在正确的顺序 – 按顺序,我们命令的function,不按照这些请求完成的顺序。

另外我们可以看到程序在4.071秒之后完成,这大概是最长的请求所花费的时间,所以我们看到请求都在同时进行。

现在,我们用async.parallelLimit来运行它, async.parallelLimit有2个并行任务的限制:

 async.parallelLimit([getA, getB, getC], 2, (err, results) => { console.log('async.parallel callback called'); if (err) { console.log('async.parallel error:', err); } else { console.log('async.parallel results:', JSON.stringify(results)); } }); 

现在有点不同了 我们马上看到的是:

 getA called getA runs request getA request returned getA returns getB called getB runs request getB request returned getB returns 

所以getAgetB被调用并返回,但是getC并没有被调用。 然后经过一段时间,我们看到:

 getB calling callback getC called getC runs request getC request returned getC returns 

这表明只要getB调用callback,Async模块不再有2个任务正在进行,而是只有1个任务,并且可以启动另一个任务,即getC ,并立即执行。

然后我们又看到:

 getC calling callback getA calling callback async.parallel callback called async.parallel results: ["Response A","Response B","Response C"] 

它完成整个过程,就像在async.parallel示例中一样。 这一次整个过程也花费了大约4秒钟,因为getC的延迟调用没有任何区别 – 它仍然在第一个调用getA之前完成。

但是,如果我们改变这些延迟:

 const delays = { A: 4000, B: 2000, C: 3000 }; 

那么情况就不一样了。 现在async.parrallel需要4秒,但async.parallelLimit与2的限制需要5秒,顺序略有不同。

没有限制:

 $ time node example.js getA called getA runs request getA request returned getA returns getB called getB runs request getB request returned getB returns getC called getC runs request getC request returned getC returns getB calling callback getC calling callback getA calling callback async.parallel callback called async.parallel results: ["Response A","Response B","Response C"] real 0m4.075s user 0m0.070s sys 0m0.009s 

限制为2:

 $ time node example.js getA called getA runs request getA request returned getA returns getB called getB runs request getB request returned getB returns getB calling callback getC called getC runs request getC request returned getC returns getA calling callback getC calling callback async.parallel callback called async.parallel results: ["Response A","Response B","Response C"] real 0m5.075s user 0m0.057s sys 0m0.018s 

概要

我认为最重要的是要记住 – 无论是使用像这种情况下的callback,还是承诺或asynchronous/等待,在单线程事件循环中,一次只能做一件事,但是可以等待很多事情在同一时间。