Node.js 原生 Promise.all 是并行处理还是顺序处理?

IT技术 javascript node.js promise es6-promise
2021-01-28 13:42:54

我想澄清这一点,因为文档对此不太清楚;

Q1:Promise.all(iterable)按顺序还是并行处理所有的promise?或者,更具体地说,它是否相当于运行链式Promise,如

p1.then(p2).then(p3).then(p4).then(p5)....

或者它是另一种算法,其中 all p1, p2, p3, p4,p5等同时(并行)被调用,并且在所有解决(或一个拒绝)后立即返回结果?

Q2:如果Promise.all并行运行,有没有方便的方式来顺序运行一个可迭代对象?

注意:我不想使用 Q 或 Bluebird,而是所有原生 ES6 规范。

6个回答

是否正在Promise.all(iterable)执行所有Promise?

不,Promise不能“被执行”。它们在被创建时就开始了它们的任务——它们只代表结果——并且甚至在将它们传递给Promise.all.

Promise.all等待多个Promise。它不关心它们以什么顺序解析,或者计算是否并行运行。

有没有一种方便的方法来按顺序运行可迭代对象?

如果你已经有你的Promise,你不能做太多但是Promise.all([p1, p2, p3, …])(它没有顺序的概念)。但是,如果您确实有可迭代的异步函数,则确实可以按顺序运行它们。基本上你需要从

[fn1, fn2, fn3, …]

fn1().then(fn2).then(fn3).then(…)

解决方案是使用Array::reduce

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())
@SSHThis:与then序列完全一样- 返回值是最后一个fn结果的Promise,您可以将其他回调链接到它。
2021-04-01 13:42:54
@wojjas 这完全等同于fn1().then(p2).then(fn3).catch(…? 无需使用函数表达式。
2021-04-03 13:42:54
@wojjas 当然retValFromF1被传入p2,这正是p2它的作用。当然,如果你想做更多(传递额外的变量,调用多个函数等),你需要使用函数表达式,尽管p2在数组中更改会更容易
2021-04-07 13:42:54
在这个例子中,iterable 是一个返回你想要调用的 promise 的函数数组吗?
2021-04-09 13:42:54
@robe007 是的,我的意思是那iterable[fn1, fn2, fn3, …]数组
2021-04-09 13:42:54

在平行下

await Promise.all(items.map(async (item) => { 
  await fetchItem(item) 
}))

优点:更快。即使稍后失败,所有迭代也将开始。但是,它会“快速失败”。使用Promise.allSettled, 并行完成所有迭代,即使某些迭代失败。

按顺序

for (const item of items) {
  await fetchItem(item)
}

优点:循环中的变量可以被每次迭代共享。表现得像普通的命令式同步代码。

@david_adler 在并行示例优势中,您说即使失败也将执行所有迭代如果我没有错,这仍然会很快失败。要更改此行为,可以执行以下操作: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
2021-03-15 13:42:54
或者: for (const item of items) await fetchItem(item);
2021-03-21 13:42:54
请注意,由于 javascript 是单线程的,因此 javascript 实际上并没有使用线程以“并行”方式执行异步请求。developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
2021-03-22 13:42:54
@Taimoor 是的,它确实“快速失败”并在 Promise.all 之后继续执行代码,但仍然执行所有迭代codepen.io/mfbx9da4/pen/BbaaXr
2021-03-28 13:42:54
这种方法更好,当async函数是 API 调用并且您不想对服务器进行 DDOS 时。您可以更好地控制执行中抛出的各个结果和错误。更好的是,您可以决定继续哪些错误以及中断循环的内容。
2021-04-09 13:42:54

NodeJS 不会并行运行 promise,它会并发运行它们,因为它是单线程事件循环架构。通过创建一个新的子进程来利用多核 CPU,可以并行运行事物。

并行与并发

事实上,Promise.all它的作用是将 promises 函数堆叠在适当的队列中(参见事件循环架构)并发运行它们(调用 P1,P2,...)然后等待每个结果,然后用所有的 promise 解析 Promise.all结果。Promise.all 将在第一个失败的Promise中失败,除非你自己管理了拒绝。

并行和并发有一个很大的区别,第一个会在不同的进程中同时运行不同的计算,它们会按照那里的节奏进行,而另一个会一个接一个地执行不同的计算,而不需要等待前一个计算同时完成和进行而不相互依赖。

最后,回答你的问题,Promise.all不会并行或顺序执行,而是并发执行。

迄今为止的最佳答案。我很困惑,像 Node.js 这样的单线程架构如何并行运行多个 promise。非常感谢先生。PS 我知道工作线程是如何工作的,但Promise是由 Node.js 事件循环本身解决的,而不是使用 libuv。因此,Node.js 能做的最好的事情就是并发执行它们(Promise)。
2021-03-16 13:42:54
这个不对。NodeJS 可以并行运行。NodeJS 有一个工作线程的概念。默认情况下,工作线程的数量是 4。例如,如果您使用加密库来散列两个值,那么您可以并行执行它们。两个工作线程将处理该任务。当然,您的 CPU 必须是多核的才能支持并行性。
2021-03-20 13:42:54
是的,你说得对,这就是我在第一段末尾所说的,但我谈到了子进程,它们当然可以运行工人。
2021-04-05 13:42:54

Bergi回答使我使用Array.reduce.

然而,为了真正让函数返回我的Promise以一个接一个地执行,我不得不添加更多的嵌套。

我的实际用例是一组文件,由于下游限制,我需要依次传输这些文件...

这是我最终的结果:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(() => {
            return transferFile(theFile); //function returns a promise
        });
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

正如以前的答案所暗示的那样,使用:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(transferFile(theFile));
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

在开始另一个之前没有等待传输完成,并且“所有文件传输”文本甚至在第一个文件传输开始之前就出现了。

不确定我做错了什么,但想分享对我有用的东西。

编辑:自从我写了这篇文章,我现在明白为什么第一个版本不起作用了。then()期望一个函数返回一个Promise。所以,你应该传入不带括号的函数名!现在,我的函数需要一个参数,所以我需要包含在一个不带参数的匿名函数中!

您还可以使用递归函数通过异步函数按顺序处理可迭代对象。例如,给定一个a要使用异步函数处理的数组someAsyncFunction()

var a = [1, 2, 3, 4, 5, 6]

function someAsyncFunction(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("someAsyncFunction: ", n)
      resolve(n)
    }, Math.random() * 1500)
  })
}

//You can run each array sequentially with: 

function sequential(arr, index = 0) {
  if (index >= arr.length) return Promise.resolve()
  return someAsyncFunction(arr[index])
    .then(r => {
      console.log("got value: ", r)
      return sequential(arr, index + 1)
    })
}

sequential(a).then(() => console.log("done"))

usingarray.prototype.reduce在性能方面比递归函数好得多
2021-03-15 13:42:54
@MateuszSowiński,每次通话之间有 1500 毫秒的超时时间。考虑到这是按顺序进行异步调用,即使对于非常快速的异步周转,也很难看出这有什么关系。
2021-03-19 13:42:54
@MateuszSowiński,堆栈不会在这里结束......我们每次调用后都会返回。将其与reduce您必须then()一步构建整个链然后执行的情况进行比较。
2021-03-20 13:42:54
假设您必须一个接一个地执行 40 个非常快速的异步函数 - 使用递归函数会非常快地阻塞您的内存
2021-04-07 13:42:54
在顺序函数的第 40 次调用中,该函数的第一次调用仍在内存中等待顺序函数链返回
2021-04-07 13:42:54