Async.js - 并行真的是并行的吗?

Posted

技术标签:

【中文标题】Async.js - 并行真的是并行的吗?【英文标题】:Async.js - Is parallel really parallel? 【发布时间】:2013-10-02 04:04:51 【问题描述】:

据我所知:javascript 是单线程的。如果您推迟执行某些过程,您只需安排它(排队)在线程空闲时运行。但是Async.js定义了两个方法:Async::parallel & Async::parallelLimit,我引用:

并行(任务,[回调])

并行运行一组函数,无需等到前一个函数完成。如果任何函数将错误传递给其回调...

parallelLimit(tasks, limit, [callback])

与并行相同,只是任务是并行执行的,在任何时候都有最多“限制”任务执行。

就我对英语的理解而言,当您说:“并行执行任务”意味着同时执行它们 - 同时进行。

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

【问题讨论】:

操作系统如何模拟单处理器机器上的多任务处理?答案是一样的:时间切片。 我对操作系统内部不太熟悉,但是在单线程中运行的 javascript 具有事件循环,该循环不断监视新事件并一个接一个地执行任何绑定的过程。没有什么是同时进行的。如果我错了,请纠正我。 你是对的。只有事情同时发生的错觉,因为顺序运行并产生另一个的短代码段与并行性非常相似(从我们的角度来看)。 所有 async 所做的就是让每个函数产生_可能并行运行的进程/工作者。如果您只是在那些功能中运行同步代码,那是您的错,而不是异步的;) @FrédéricHamidi 所以这些方法的命名并不能完全描述它的真正作用? 【参考方案1】:

这些函数不会同时执行,但是当第一个函数移交给异步任务(例如 setTimeout、network、...)时,第二个函数将启动,即使第一个函数没有调用提供的回调。

至于并行任务的数量:这取决于您选择的内容。

【讨论】:

【参考方案2】:

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

parallel 同时运行所有任务。因此,如果您的任务包含 I/O 调用(例如查询数据库),它们将看起来好像它们已被并行处理。

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

Node.js 是非阻塞的。因此,它不是并行处理所有任务,而是从一个任务切换到另一个任务。因此,当第一个任务进行 I/O 调用时,Node.js 会简单地切换到处理另一个任务。

I/O 任务的大部分处理时间都在等待 I/O 调用的结果。在像 Java 这样的阻塞语言中,这样的任务在等待结果时会阻塞其线程。但是 Node.js 会利用它来处理其他任务而不是等待。

所以这意味着如果每个任务的内部处理是异步的,那么线程将被授予该任务的每个位,无论他们中的任何人是否已经完成,直到所有人都完成了他们的位?

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

【讨论】:

这对我来说解释了很多。我每个都使用异步,但是,根据我的控制台日志,它没有重新排序任何东西(就像在数组中以相同顺序在另一件事之前完成一样)。 没有真正的“平行”。一次只会发生一件事。只有当一个人暂停时,另一个人介入并等待暂停结束。所以,仅仅运行console.log 并不足以阻止它。你可以说它是更好的时间管理,但我不会称之为并行。 附带说明,有没有办法在节点中实现真正的 parralisem,也许是子进程? @eranotzap 是的,这是可能的。您可以使用多个独立的 node.js 工作线程,也可以使用 fibers module 提供的绿色线程。您可以使用child_process.fork() 或cluster.fork() 生成worker。 @LeonidBeschastny,例如,如果您需要处理大型数据集,是否需要在这些节点或子进程之上实施某种分片机制? @eranotzap 这取决于您尝试执行的实际任务。如果此处理需要大量 CPU 工作,那么您可以使用 child_process.fork() 生成一个独立的 node.js 工作程序,以在单独的进程中执行整个操作。虽然,使用 node.js 执行 CPU 密集型处理并不是一个好主意,但其他工具可能更适合您的需求。但如果这个处理主要由 I/O 操作(api 调用、db 查询)组成,那么单个 Node.js 进程将处理得很好。【参考方案3】:

就我对英语的理解而言,当您说:“并行执行任务”意味着同时执行它们 - 同时进行。

正确。而“同时”的意思是“至少有一个时刻,两个或多个任务已经开始,但尚未完成”。

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

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

【讨论】:

【参考方案4】:

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

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

【讨论】:

【参考方案5】:

您的怀疑完全有道理。您问这个问题已经有几年了,但我认为值得对现有答案添加一些想法。

并行运行一组函数,无需等到前一个函数完成。如果任何函数将错误传递给其回调...

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

寓言

前段时间我写了一篇短篇小说来证明这个概念:

Nonblacking I/O on the planet Asynchronia256/16

引用其中的一部分:

“所以我说:‘等一下,你告诉我一个蛋糕需要三个半小时,而四个蛋糕只需要一个多半小时?这没有任何意义!’我想她一定是在开玩笑,所以我开始笑。” “但她不是在开玩笑吗?” “不,她看着我说:‘这完全有道理。这个时间主要是在等待。我可以一次等待很多事情。我停止了笑,开始思考。它终于开始影响我了。同时做四个枕头并没有给你带来任何时间,也许可以说它更容易组织,但话又说回来,也许不是。但这一次,情况有所不同。但我还不知道如何使用这些知识。”

理论

我认为需要强调的是,在单线程事件循环中,您一次只能做一件事情。但是你可以一次等待很多事情就好了。这就是这里发生的事情。

Async 模块中的并行函数一个一个地调用每个函数,但是每个函数都必须在调用下一个函数之前返回,没有办法绕过它。这里的神奇之处在于该函数在返回之前并没有真正完成它的工作 - 它只是安排一些任务,注册一个事件监听器,在其他地方传递一些回调,向一些承诺添加一个解析处理程序等。

然后,当计划任务完成时,执行该函数先前注册的某个处理程序,这将依次执行最初由 Async 模块传递的回调,并且 Async 模块知道该函数已完成 - this时间不仅在某种意义上它返回了,而且传递给它的回调最终被调用了。

示例

例如,假设您有 3 个函数下载 3 个不同的 URL:getA()getB()getC()

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

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

现在这 3 个函数基本相同,带有详细的日志记录:

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 调用我们的回调,所有响应都以正确的顺序组合在一起 - 在按我们订购函数的顺序,而不是按照这些请求完成的顺序。

我们还可以看到程序在 4.071 秒后结束,这大约是最长请求所用的时间,因此我们看到所有请求都在同一时间进行。

现在,让我们使用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 调用回调,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 秒,但限制为 2 的async.parallelLimit 需要 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

总结

我认为要记住的最重要的事情 - 无论您使用这种情况下的回调,还是使用 Promise 或 async/await,在单线程事件循环中您一次只能做一件事,但您可以同时等待很多事情。

【讨论】:

以上是关于Async.js - 并行真的是并行的吗?的主要内容,如果未能解决你的问题,请参考以下文章

Node.js - Async.js:并行执行如何工作?

flink 并行度

多线程编程

有人可以帮我并行化这个 C++ 代码吗?

并发与并行

Delayed_job - 多个并行队列?