Promise.all 是否并行运行承诺?

Rap*_*ida 2 javascript parallel-processing node.js promise

我看到很多人说 Promise.all 无法实现并行性,因为 node/javascript 运行在单线程环境上。然而,例如,如果我将 5 个 Promise 包装在 Promise.all 中,其中每个 Promise 在 3 秒后解析(一个简单的 setTimeout Promise),那么为什么 Promise.all 在 3 秒内解析所有它们而不是大约 15 秒(每次 5 x 3 秒)?

请参阅下面的示例:

function await3seconds () { 
    return new Promise(function(res) {
        setTimeout(() => res(), 3000)
    })
}

console.time("Promise.all finished in::")

Promise.all([await3seconds(), await3seconds(), await3seconds(), await3seconds(), await3seconds()])
    .then(() => {
        console.timeEnd("Promise.all finished in::")
})
Run Code Online (Sandbox Code Playgroud)

它记录:

Promise.all finished in::: 3.016s
Run Code Online (Sandbox Code Playgroud)

如果没有并行性,这种行为怎么可能?并发执行也无法在 3 秒内处理所有这些 Promise。

jfr*_*d00 8

了解这行代码的实际作用特别有用:

Promise.all([await3seconds(), await3seconds(), await3seconds(), await3seconds(), await3seconds()]).then(...)
Run Code Online (Sandbox Code Playgroud)

这基本上与:

const p1 = await3seconds();
const p2 = await3seconds();
const p3 = await3seconds();
const p4 = await3seconds();
const p5 = await3seconds();

Promise.all([p1, p2, p3, p4, p5]).then(...)
Run Code Online (Sandbox Code Playgroud)

我在这里试图展示的是,您的所有函数都按照声明的顺序一个接一个地串行执行,并且它们在执行之前Promise.all()都已返回。

因此,得出一些结论:

  1. Promise.all()没有“运行”任何东西。它接受一系列承诺,并且只是监视所有这些承诺,按顺序收集它们的结果,并在它们全部完成时通知您(通过.then()await),或者在第一个承诺拒绝时告诉您。
  2. 您的函数已经执行并在Promise.all()运行之前返回了一个承诺。因此,Promise.all()并不能决定这些函数如何运行。
  3. 如果您调用的函数是阻塞的,则第一个函数将在调用第二个函数之前运行完成。Promise.all()同样,这与函数在Promise.all()调用之前全部执行之前无关。
  4. 在您的特定示例中,您的函数每个都会启动一个计时器并立即返回。因此,您实际上在几毫秒内启动了 5 个计时器,这些计时器都设置为在 3 秒内触发。 setTimeout()是非阻塞的。它告诉系统创建一个计时器,并为系统提供一个回调,以便在该计时器触发时调用,然后立即返回。稍后,当事件循环空闲时,计时器将触发并调用回调。因此,这就是为什么所有计时器都会同时设置并且几乎同时触发。如果您希望它们每个间隔 3 秒,则必须以不同的方式编写此代码,要么为每个计时器设置增加的时间,要么在第一个计时器触发之前不启动第二个计时器,依此类推。

因此,Promise.all()您可以监视多个异步操作,这些操作本身(独立于Promise.all())能够异步运行。Nodejs 本身与 无关Promise.all(),具有并行运行多个异步操作的能力。例如,您可以发出多个 http 请求或从文件系统发出多个读取请求,nodejs 将并行运行这些请求。他们将同时“飞行”。

因此,Promise.all()并没有启用异步操作的并行性。该功能内置于异步操作本身以及它们与 Nodejs 交互的方式以及它们的实现方式中。 Promise.all()允许您跟踪多个异步操作并知道它们何时全部完成,按顺序获取其结果和/或知道其中一项操作何时出现错误。


如果您好奇计时器在 NodeJS 中如何工作,它们是由 libuv 管理的,libuv 是一个跨平台库,NodeJS 使用它来管理事件循环、计时器、文件系统访问、网络和一大堆东西。

在 libuv 内部,它管理待处理定时器的排序列表。计时器按下次触发时间排序,因此最早触发的计时器位于列表的开头。

Nodejs 中的事件循环会循环检查一系列不同的事情,其中​​之一就是查看计时器列表的当前头是否已达到其触发时间。如果是,它会从列表中删除该计时器,获取与该计时器关联的回调并调用它。

其他类型的异步操作(例如文件系统访问)的工作方式完全不同。模块中的异步文件操作fs,实际上使用了一个本地代码线程池。因此,当您请求异步文件操作时,它实际上从线程池中获取一个线程,为其提供您请求的特定文件操作的作业,然后继续发送该线程。然后,该本机代码线程独立于 Javascript 解释器运行,可以自由地执行其他操作。在将来的某个时间,当线程完成文件操作时,它会调用事件循环的线程安全接口以将文件完成事件添加到队列中。当当前正在执行的任何 Javascript 完成并将控制权返回给事件循环时,事件循环要做的事情之一就是检查是否有任何文件系统完成事件等待执行。如果是,它将从队列中删除它并调用与其关联的回调。

因此,虽然如果设计得当(通常由本机代码支持),各个异步操作本身可以并行运行,但向 Javascript 发出完成或错误信号都通过事件循环运行,并且解释器一次仅运行一段 Javascript。

注意:为了讨论的目的,这忽略了WorkerThread实际上启动一个完全不同的解释器并且可以同时运行多组 Javascript 的功能。但是,每个单独的解释器仍然以单线程方式运行 Javascript,并且仍然通过事件循环与外界协调。