等待多个并发等待操作

IT技术 javascript promise async-await ecmascript-2017
2021-01-16 15:12:09

如何更改以下代码,以便触发两个异步操作并有机会同时运行?

const value1 = await getValue1Async();
const value2 = await getValue2Async();
// use both values

我需要做这样的事情吗?

const p1 = getValue1Async();
const p2 = getValue2Async();
const value1 = await p1;
const value2 = await p2;
// use both values
4个回答

TL; 博士

不要在你得到promise的问题中使用模式,然后单独等待它们;相反,使用Promise.all(至少现在):

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

虽然您的解决方案确实并行运行这两个操作,但如果两个Promise都拒绝,它就无法正确处理拒绝。

细节:

您的解决方案并行运行它们,但始终等待第一个完成,然后再等待第二个。如果你只是想启动它们,并行运行它们,并获得两个结果,那很好。 (不,不是,请继续阅读...)请注意,如果第一个需要(例如)五秒钟才能完成,而第二个在一秒钟内失败,则您的代码将等待整整五秒钟,然后才会失败。

可悲的是,目前await没有进行并行等待的语法,所以你有你列出的尴尬,或者Promise.all. 不过,已经讨论过await.all或 类似的;也许有一天。)

Promise.all版本是:

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

...更简洁,如果第二个操作快速失败,也不会等待第一个操作完成(例如,在我上面的五秒/一秒示例中,上面将在一秒内拒绝而不是等待五秒) . 另请注意,对于您的原始代码,如果第二个Promise在第一个Promise解决之前拒绝,您很可能会在控制台中收到“未处理的拒绝”错误(您目前使用 Chrome v61;更新:更新的版本有更有趣的行为)虽然这个错误可以说是虚假的(因为你这样做,最终处理的拒绝,在此代码显然是在asyncfunction¹,因此该功能将钩拒绝做出Promise拒绝使用它)(更新:再次,改变了)。但是如果两个Promise都拒绝,你会得到一个真正的未处理的拒绝错误,因为控制流永远不会到达const value2 = await p2;,因此 p2 拒绝永远不会被处理。

未处理的拒绝是一件坏事™(以至于很快,Node.js 将中止真正未处理的拒绝的过程,就像未处理的异常一样——因为它们就是这样),所以最好避免“得到Promise然后await它”你的问题中的模式。

这是失败情况下时间差异的示例(使用 500 毫秒和 100 毫秒,而不是 5 秒和 1 秒),并且可能还有可以说是虚假的未处理拒绝错误(打开真实浏览器控制台查看):

const getValue1Async = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 500, "value1");
  });
};
const getValue2Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, "error");
  });
};

// This waits the full 500ms before failing, because it waits
// on p1, then on p2
(async () => {
  try {
    console.time("separate");
    const p1 = getValue1Async();
    const p2 = getValue2Async();
    const value1 = await p1;
    const value2 = await p2;
  } catch (e) {
    console.error(e);
  }
  console.timeEnd("separate");
})();

// This fails after just 100ms, because it doesn't wait for p1
// to finish first, it rejects as soon as p2 rejects
setTimeout(async () => {
  try {
    console.time("Promise.all");
    const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);
  } catch (e) {
    console.timeEnd("Promise.all", e);
  }
}, 1000);
Open the real browser console to see the unhandled rejection error.

在这里,我们拒绝p1p2,导致 上的非虚假未处理拒绝错误p2


在您问过的评论中:

附带问题:以下强制是否会同时等待(并丢弃结果)await p1 && await p2

这与原始代码在Promise拒绝方面存在相同的问题:p1即使p2更早拒绝,它也会等到解决如果解决之前拒绝,它可能会产生一个可以说是虚假的(更新: 或临时)未处理的拒绝错误和它产生如果两个真正未处理拒绝错误拒绝(因为的拒绝从未处理)。p2p1p1p2p2

这是p1解决和p2拒绝的情况

...并且两者都拒绝:


¹ “......这段代码显然在一个async函数中......”在 2017 年编写此问题和答案时确实如此。从那时起,顶层await发生/正在发生。

@TJCrowder 在这种情况下,“我们的代码”是 Node 项目。特别是这是我参与的代码领域 - 对于含糊不清的地方感到抱歉。下面是我们的做法:github.com/nodejs/node/blob/master/lib/internal/process/... - 有github.com/nodejs/node/pull/15126github.com/nodejs/node/pull /15335关于正在进行的工作。在 Chrome 中,您可以在github.com/nwjs/chromium.src/blob/...看到 V8 绑定,它在任务后在 ProcessQueue 中运行。
2021-03-10 15:12:09
“(以至于很快,NodeJS 将在未处理的拒绝时中止进程,就像未处理的异常一样——因为它们就是这样)”——弃用警告措辞令人遗憾,我对此感到遗憾——但我们永远不会杀死代码上的节点上面 - 我们将:A)基于 GC 进行未处理的拒绝 B)警告GC 错过的非常长的挂起操作,可能 C)只有在我们能证明拒绝未处理(第一种情况)时才会终止 Node.js。我意识到情况令人困惑,我为此道歉 - 我们会做得更好。
2021-03-18 15:12:09
await p1 && await p2如果两个p1和p2拒绝然后p2为未处理的排斥反应(以及基于GC检测将仍然杀死理所当然的过程)。我只是在谈论 p2 拒绝而 p1 仍在等待中的情况。
2021-03-20 15:12:09
@Ben:你的和Promise.all我刚刚编辑的有一个重要区别,仅供参考。
2021-04-02 15:12:09
也就是说,没人能“基于GC”(火狐确实在一个点上,不知道他们是否仍然这样做) - BridgeAR的PR显示了我们正在考虑对现在的做法。区域也可能是一个有趣的想法。
2021-04-09 15:12:09

我认为这应该有效:

 const [value1, value2] = await Promise.all([getValue1Async(),getValue2Async()]);

下面是一个更详细的示例,以帮助理解:

const promise1 = async() => {
  return 3;
}

const promise2 = async() => {
  return 42;
}

const promise3 = async() => {
  return 500;
  // emulate an error
  // throw "something went wrong...";
}

const f1 = async() => {

  try {
    // returns an array of values
    const results = await Promise.all([promise1(), promise2(), promise3()]);
    console.log(results);
    console.log(results[0]);
    console.log(results[1]);
    console.log(results[2]);

    // assigns values to individual variables through 'array destructuring'
    const [value1, value2, value3] = await Promise.all([promise1(), promise2(), promise3()]);

    console.log(value1);
    console.log(value2);
    console.log(value3);

  } catch (err) {
    console.log("there was an error: " + err);
  }

}

f1();

我明白了你的想法。恕我直言,它应该工作:)。抱歉我粗心的确认
2021-03-20 15:12:09

使用 .catch() 和 Promise.all()

确保正确处理拒绝,并且可以安全地使用 Promises.all() 而不会面临未处理的拒绝。(编辑:每个讨论的澄清:不是错误,unhandled rejection而只是代码未处理的Promise.all()拒绝将抛出第一个Promise拒绝并忽略其余部分)。

在下面的示例中,返回 [[error, results], ...] 数组以方便处理结果和/或错误。

let myTimeout = (ms, is_ok) =>
  new Promise((resolve, reject) => 
    setTimeout(_=> is_ok ? 
                   resolve(`ok in ${ms}`) :
                   reject(`error in ${ms}`),
               ms));

let handleRejection = promise => promise
  .then((...r) => [null, ...r])
  .catch(e => [e]); 

(async _=> {
  let res = await Promise.all([
    myTimeout(100, true),
    myTimeout(200, false),
    myTimeout(300, true),
    myTimeout(400, false)
  ].map(handleRejection));
  console.log(res);
})();

您可以从 catch() 中抛出以停止等待所有结果(并丢弃其余的结果),但是 - 您只能在每个 try/catch 块中执行一次,因此需要维护和检查标志 has_thorwn 以确保没有未处理的错误发生。

@Bergi - 如果不正确处理拒绝,则无法避免未处理的Promise拒绝(在已接受的答案中进行了大量讨论),这将(在未来)终止节点进程。模式 [err, results] 只是最后如何传递和处理多个错误的示例。
2021-03-13 15:12:09
不,您不需要.catch()单独的Promise,Promise.all完全有能力防止对它们的未经处理的拒绝(如已接受的答案中所述)。
2021-03-14 15:12:09
我认为这并不能真正回答问题,并且catch在这个位置确实没有必要避免未经处理的拒绝。此外,这种[error, results]模式是一个非常糟糕的主意
2021-03-15 15:12:09
@Bergi,关于回答这个问题:Promise.all() 没有回答?此外,“......并有机会同时运行” - 如果没有正确处理,如果一个被拒绝,其他人没有机会返回结果。
2021-03-16 15:12:09
如果一个被拒绝,其他人就没有机会返回结果” - 这是一个完全不同的问题
2021-03-24 15:12:09

解决而不是Promise

const wait = (ms, data) => new Promise( resolve => setTimeout(resolve, ms, data) )
const reject = (ms, data) => new Promise( (r, reject) => setTimeout(reject, ms, data) )
const e = e => 'err:' + e
const l = l => (console.log(l), l)

;(async function parallel() {

  let task1 = reject(500, 'parallelTask1').catch(e).then(l)
  let task2 = wait(2500, 'parallelTask2').catch(e).then(l)
  let task3 = reject(1500, 'parallelTask3').catch(e).then(l)

  console.log('WAITING')

  ;[task1, task2, task3] = [await task1, await task2,  await task3]

  console.log('FINISHED', task1, task2, task3)

})()

正如其他答案中指出的那样,被拒绝的Promise可能会引发未处理的异常。
.catch(e => e)是一个巧妙的小技巧,它可以捕获错误并将其传递到链中,从而允许Promiseresolve, 而不是rejecting

如果您发现此 ES6 代码丑陋,请在此处查看更友好的内容