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

15
据我目前的理解: Javascript是单线程的。如果你延迟了某个过程的执行,你只是安排它(将其排队),以便在线程空闲时立即运行。但是Async.js定义了两种方法:Async::parallel & Async::parallelLimit,我引用如下:
  • parallel(tasks, [callback])

并行运行一组函数,而不等待上一个函数完成。如果任何一个函数传递给它的回调函数出现错误...

  • parallelLimit(tasks, limit, [callback])

与parallel相同,只有任务以最多"limit"个任务同时执行的方式并行执行。

据我的英语理解,当你说“同时进行任务”时,意味着同时进行-同时进行。
Async.js如何在单线程中并行执行任务? 我有什么遗漏吗?

1
操作系统如何在单处理器机器上模拟多任务处理?答案是相同的:时间片轮转。 - Frédéric Hamidi
我对操作系统内部不太熟悉,但是在单线程上运行的JavaScript具有事件循环,该循环不断监视新事件并逐个执行任何与其绑定的过程。没有同时执行的情况。如果我说错了,请纠正我。 - tikider
你是对的。事情同时发生只是一种错觉,因为顺序运行的短代码片段相互产生并非常类似于并行(从我们的角度来看)。 - Frédéric Hamidi
1
所有的async只是让每个函数产生可以并行运行的进程/工作者。如果您在这些函数中只运行同步代码,那么这就是你的问题,而不是async的问题 ;) - Andreas Hultgren
@tikider,确实,这些名称仅代表方法实际执行的可见行为。 - Frédéric Hamidi
显示剩余3条评论
5个回答

15

如何在单线程中使用Async.js并行执行任务? 我有什么遗漏吗。

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

这是如何在单线程中实现的?!我不明白。

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

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

这意味着如果每个任务的内部处理都是异步的,那么无论它们是否完成其位,线程都会授予给这些任务的每一部分,直到所有任务都完成了吗?

是的,几乎就像你说的那样。Node.js 开始处理第一个任务,直到它暂停去进行 I/O 调用。在那个时刻,Node.js 就会离开这个任务,将主线程授权给另一个任务。因此,你可以说线程依次授予给每个活动任务。


4
这对我解释了很多。我一直在使用 async 的 each,但是根据我的控制台日志,它没有重新排序任何东西(比如按数组中相同顺序一个接一个地完成某个任务)。实际上,并不存在真正的“并行”。只有一个任务在任何时刻被执行。只有当一个任务暂停时,另一个任务才会接替并等待暂停结束。因此,仅仅运行 console.log 是不足以停止它的。你可以说这是更好的时间管理方式,但我不会称之为并行。 - DaAwesomeP
顺便提一下,有没有办法在Node中实现真正的并行处理,也许可以使用子进程? - eran otzap
@eranotzap 是的,这是可能的。您可以使用多个独立的node.js工作进程或由fibers模块提供的绿色线程。您可以使用child_process.fork()cluster.fork()来生成工作进程。 - Leonid Beschastny
@eranotzap 这取决于您要执行的实际任务。如果此处理需要大量的CPU工作,则可以使用child_process.fork()生成独立的node.js工作程序来在单独的进程中执行整个操作。虽然使用node.js执行CPU绑定处理不是一个很好的主意,其他工具可能更适合您的需求。但如果此处理主要由I/O操作(api调用、db查询)组成,则单个Node.js进程将处理得很好。 - Leonid Beschastny
@LeonidBeschastny 是的,这就是使用案例。 但我需要处理一个大数据集。在Node中。因为整个系统都是用node编写的,我不想偏离我们的开发堆栈。 现在我有一个需要并行处理的大型数据集 那应该如何在node中完成? - eran otzap
显示剩余2条评论

4
Async.Parallel很好地在这里进行了文档化:https://github.com/caolan/async#parallel Async.Parallel是关于并行启动I/O任务,而不是关于代码的并行执行。如果你的任务没有使用任何计时器或执行任何I/O,则它们实际上将按顺序执行。每个任务的同步设置部分将依次发生。JavaScript仍然是单线程的。

2

这些函数不会同时执行,而是当第一个函数被移交给异步任务(例如setTimeout、网络等)时,第二个函数将开始执行,即使第一个函数还没有调用提供的回调函数。

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


1

你的疑虑很有道理。虽然你提出这个问题已经过去了几年,但我认为值得在现有答案中添加一些内容。

并行运行函数数组,而不等待前一个函数完成。如果任何一个函数将错误传递给其回调…

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

比喻

不久前,我写了一个简短的故事来演示这个概念:

引用其中的一部分:

“So I said: ‘Wait a minute, you tell me that one cake takes three and a half hours and four cakes take only half an hour more than one? It doesn’t make any sense!’ I though that she must be kidding so I started laughing.”
“But she wasn’t kidding?”
“No, she looked at me and said: ‘It makes perfect sense. This time is mostly waiting. And I can wait for many things at once just fine.’ I stopped laughing and started thinking. It finally started to get to me. Doing four pillows at the same time didn’t buy you any time, maybe it was arguably easier to organize but then again, maybe not. But this time it was something different. But I didn’t really know how to use that knowledge yet.”

Theory

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

Async模块中的并行函数按顺序调用每个函数,但每个函数必须在下一个函数被调用之前返回,这是无法避免的。这里的魔法在于函数在返回之前并没有真正执行其任务 - 它只是安排了一些任务、注册了事件监听器、将某些回调传递到其他地方、向某些承诺添加了解决处理程序等。
然后,当预定的任务完成时,之前由该函数注册的某个处理程序被执行,这反过来又执行了最初由Async模块传递的回调函数,并且Async模块知道这个函数已经完成了 - 这次不仅仅是指它已经返回,而且还意味着传递给它的回调函数终于被调用了。
例如,假设您有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]);
};

现在有三个功能基本相同,采用详细记录方式:
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.parallel 需要 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

概述

我认为无论你是使用类似于回调函数的方式,还是使用 promises 或 async/await,最重要的一点是,在单线程事件循环中你只能一次做一件事情,但是你可以同时等待许多事情。


1
根据我对英语的理解,“doing tasks in parallel”意味着同时进行这些任务-即同时进行。正确。而“同时”意味着“至少有一个时刻,两个或更多任务已经开始但尚未完成”。当某个任务由于某种原因(即IO)停止时,async.js会执行另一个任务,稍后继续第一个任务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接