【问题标题】:Call async/await functions in parallel并行调用 async/await 函数
【发布时间】:2017-12-17 07:01:42
【问题描述】:

据我了解,在 ES7/ES2016 中,将多个 await 放入代码中的工作方式类似于将 .then() 与 Promise 链接在一起,这意味着它们将一个接一个地执行,而不是并行执行。因此,例如,我们有以下代码:

await someCall();
await anotherCall();

我是否正确理解 anotherCall() 只有在 someCall() 完成时才会被调用?并行调用它们的最优雅方式是什么?

我想在Node中使用它,所以也许有异步库的解决方案?

编辑:我对这个问题中提供的解决方案不满意:Slowdown due to non-parallel awaiting of promises in async generators,因为它使用了生成器,我问的是更一般的用例。

【问题讨论】:

  • @Blindman67 - 确实如此,至少 OP 的意思是,两个异步操作同时运行,但不是在这种情况下,我的意思是它们在 serial 中运行,第一个 await 将等待第一个函数完全完成,然后再执行第二个。
  • @Blindman67 - 它是单线程的,但该限制不适用于异步方法,它们可以同时运行,并在完成后返回响应,即OP 的意思是“并行”。
  • @Blindman67 - 我认为 OP 的要求很清楚,使用 async/await 模式将使函数串行运行,即使它们是异步的,所以第一个函数会在第二个被调用等等。OP正在询问如何并行调用这两个函数,并且由于它们显然是异步的,因此目的是同时运行它们,即并行运行,例如同时执行两个ajax请求,这不是问题一切都在 javascript 中,正如您所指出的,大多数异步方法运行本机代码并使用更多线程。
  • @Bergi 这不是链接问题的重复 - 这专门关于 async/await 语法和原生 Promises。链接的问题是关于带有生成器和产量的蓝鸟库。可能在概念上相似,但在实现上并不相似。
  • @Bergi 语法非常重要。对于从未使用过发电机或蓝鸟的人来说,链接的问题完全没有帮助。

标签: javascript node.js asynchronous ecmascript-6 babeljs


【解决方案1】:

您可以调用多个异步函数而无需等待它们。这将并行执行它们。这样做时,将返回的 Promise 保存在变量中,并在某个时候单独等待它们或使用 Promise.all() 并处理结果。

您还可以使用 try...catch 包装函数调用,以处理单个异步操作的失败并提供回退逻辑。

这是一个例子: 观察日志,在各个异步函数执行开始时打印的日志会立即打印,即使第一个函数需要 5 秒才能解决。

function someLongFunc () {
return new Promise((resolve, reject)=> {
console.log('Executing function 1')
setTimeout(resolve, 5000)
})
}

function anotherLongFunc () {
return new Promise((resolve, reject)=> {
console.log('Executing function 2')
setTimeout(resolve, 5000)
})
}

async function main () {
let someLongFuncPromise, anotherLongFuncPromise
const start = Date.now()
try {
someLongFuncPromise = someLongFunc()
}
catch (ex) {
console.error('something went wrong during func 1')
}
try {
anotherLongFuncPromise = anotherLongFunc()
}
catch (ex) {
console.error('something went wrong during func 2')
}

await someLongFuncPromise
await anotherLongFuncPromise
const totalTime = Date.now() - start
console.log('Execution completed in ', totalTime)
}

main()

【讨论】:

    【解决方案2】:

    2021年需要前端新星方式的读者:

    以下是有史以来最漂亮的前端 API 网关代码,它并行调用三个不同的服务,然后使用其中一个结果循环调用另一个服务,ProblemService

    请注意,我使用 awaitasyncPromise.all 的方式胜过整个 StackOverflow。

    class ExamScoreboardService {
        getExamScoreboard(examId) {
            return Promise.all([
                examService.getExaminees(examId),
                examService.getExamOverview(examId),
                examService.getExamScores(examId),
            ])
                .then(async ([examinees, exam, examScores]) => {
                    const out = {}
                    await Promise.all(exam.questions.map(async q =>
                        problemService.getProblemById(q.problemId).then(problem => out[q.problemId] = problem.testcases.length)))
                    return [examinees, exam, examScores, out]
                })
                .then(values => {
                    const [examinees, exam, examScores, totalTestcasesOf] = values;
                    return new ExamScoreboard({examinees, exam, examScores, totalTestcasesOf});
                })
        }
    }
    

    【讨论】:

    • 为什么将 async/await 与 .then 混合使用?为什么不只是异步/等待?
    • @Tom:你可以把你的版本发给我,我会检查它是否可以运行。
    • 类似的东西:jsfiddle.net/fqjgt6uk
    【解决方案3】:

    我创建了a gist 测试了一些解决承诺的不同方法,并得到了结果。查看有效的选项可能会有所帮助。

    编辑:根据Jin Lee的评论提供内容

    // Simple gist to test parallel promise resolution when using async / await
    
    function promiseWait(time) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(true);
        }, time);
    });
    }
    
    
    async function test() {
        return [
        await promiseWait(1000),
        await promiseWait(5000),
        await promiseWait(9000),
        await promiseWait(3000),
        ]
    }
    
    async function test2() {
        return {
            'aa': await promiseWait(1000),
            'bb': await promiseWait(5000),
            'cc': await promiseWait(9000),
            'dd': await promiseWait(3000),
        }
    }
    
    async function test3() {
        return await {
            'aa': promiseWait(1000),
            'bb': promiseWait(5000),
            'cc': promiseWait(9000),
            'dd': promiseWait(3000),
        }
    }
    
    async function test4() {
        const p1 =  promiseWait(1000);
        const p2 =  promiseWait(5000);
        const p3 =  promiseWait(9000);
        const p4 =  promiseWait(3000);
        return {
            'aa': await p1,
            'bb': await p2,
            'cc': await p3,
            'dd': await p4,
        };
    }
    
    async function test5() {
        return await Promise.all([
                                 await promiseWait(1000),
                                 await promiseWait(5000),
                                 await promiseWait(9000),
                                 await promiseWait(3000),
                                 ]);
    }
    
    async function test6() {
        return await Promise.all([
                                 promiseWait(1000),
                                 promiseWait(5000),
                                 promiseWait(9000),
                                 promiseWait(3000),
                                 ]);
    }
    
    async function test7() {
        const p1 =  promiseWait(1000);
        const p2 =  promiseWait(5000);
        const p3 =  promiseWait(9000);
        return {
            'aa': await p1,
            'bb': await p2,
            'cc': await p3,
            'dd': await promiseWait(3000),
        };
    }
    
    let start = Date.now();
    
    test().then((res) => {
        console.log('Test Done, elapsed', (Date.now() - start) / 1000, res);
    
        start = Date.now();
        test2().then((res) => {
            console.log('Test2 Done, elapsed', (Date.now() - start) / 1000, res);
    
            start = Date.now();
            test3().then((res) => {
                console.log('Test3 Done, elapsed', (Date.now() - start) / 1000, res);
    
                start = Date.now();
                test4().then((res) => {
                    console.log('Test4 Done, elapsed', (Date.now() - start) / 1000, res);
    
                    start = Date.now();
                    test5().then((res) => {
                        console.log('Test5 Done, elapsed', (Date.now() - start) / 1000, res);
    
                        start = Date.now();
                        test6().then((res) => {
                            console.log('Test6 Done, elapsed', (Date.now() - start) / 1000, res);
                        });
    
                        start = Date.now();
                        test7().then((res) => {
                            console.log('Test7 Done, elapsed', (Date.now() - start) / 1000, res);
                        });
                    });
                });
    
            });
        });
    
    });
    /*
    Test Done, elapsed 18.006 [ true, true, true, true ]
    Test2 Done, elapsed 18.009 { aa: true, bb: true, cc: true, dd: true }
    Test3 Done, elapsed 0 { aa: Promise { <pending> },
      bb: Promise { <pending> },
      cc: Promise { <pending> },
      dd: Promise { <pending> } }
    Test4 Done, elapsed 9 { aa: true, bb: true, cc: true, dd: true }
    Test5 Done, elapsed 18.008 [ true, true, true, true ]
    Test6 Done, elapsed 9.003 [ true, true, true, true ]
    Test7 Done, elapsed 12.007 { aa: true, bb: true, cc: true, dd: true }
    */
    

    【讨论】:

    • gist 中的测试 4 和 6 返回了预期的结果。请参阅 NoN​​ameProvided 的 stackoverflow.com/a/42158854/5683904,他解释了选项之间的区别。
    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
    • @JinLee 根据您的建议,我已经添加了内容,最初没有添加,因为它看起来很多内容
    • @SkarXa SO 现在会更喜欢你的回答。 :) 而且您的代码并不长。别担心。谢谢!
    【解决方案4】:

    您可以在Promise.all() 上等待:

    await Promise.all([someCall(), anotherCall()]);
    

    存储结果:

    let [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);
    

    请注意,Promise.all 快速失败,这意味着一旦提供给它的承诺之一被拒绝,那么整个事情都会被拒绝。

    const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
    const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))
    
    Promise.all([happy('happy', 100), sad('sad', 50)])
      .then(console.log).catch(console.log) // 'sad'

    相反,如果您想等待所有承诺履行或拒绝,那么您可以使用Promise.allSettled。请注意,Internet Explorer 本身并不支持此方法。

    const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
    const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))
    
    Promise.allSettled([happy('happy', 100), sad('sad', 50)])
      .then(console.log) // [{ "status":"fulfilled", "value":"happy" }, { "status":"rejected", "reason":"sad" }]

    注意:如果您使用Promise.all 在拒绝发生之前成功完成的操作不会回滚,因此您可能需要处理这种情况。例如 如果您有 5 个动作,4 个快速,1 个慢速和慢速拒绝。那4个 操作可能已经执行,因此您可能需要回滚。在这种情况下,请考虑使用Promise.allSettled,它会提供哪些操作失败和哪些操作失败的确切细节。

    【讨论】:

    • 干净但要注意 Promise.all 的快速失败行为。如果任何函数抛出错误,Promise.all 将拒绝
    • 您可以使用 async/await 很好地处理部分结果,请参阅 stackoverflow.com/a/42158854/2019689
    • 专业提示:使用数组解构从 Promise.all() 初始化任意数量的结果,例如:[result1, result2] = Promise.all([async1(), async2()]);
    • @jonny 这个主题是快速失败的吗?另外,还需要= await Promise.all吗?
    • @theUtherSide 你说得对——我忽略了等待。
    【解决方案5】:

    await Promise.all([someCall(), anotherCall()]);正如已经提到的,它将充当线程围栏(在并行代码中非常常见,如 CUDA),因此它将允许其中的所有 Promise 运行而不会相互阻塞,但会阻止执行继续,直到 ALL 解决。

    另一个值得分享的方法是 Node.js 异步,如果任务直接链接到使用有限资源作为 API 调用,它还允许您轻松控制通常需要的并发量,I/ O操作等

    // create a queue object with concurrency 2
    var q = async.queue(function(task, callback) {
      console.log('Hello ' + task.name);
      callback();
    }, 2);
    
    // assign a callback
    q.drain = function() {
      console.log('All items have been processed');
    };
    
    // add some items to the queue
    q.push({name: 'foo'}, function(err) {
      console.log('Finished processing foo');
    });
    
    q.push({name: 'bar'}, function (err) {
      console.log('Finished processing bar');
    });
    
    // add some items to the queue (batch-wise)
    q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
      console.log('Finished processing item');
    });
    
    // add some items to the front of the queue
    q.unshift({name: 'bar'}, function (err) {
      console.log('Finished processing bar');
    });
    

    感谢 Medium 文章作者 (read more)

    【讨论】:

    • bluebirds .map、.filter 和 .mapSeries 如果您不想引入异步模块回调 api,也有助于并发性
    【解决方案6】:

    就我而言,我有几个要并行执行的任务,但我需要对这些任务的结果做一些不同的事情。

    function wait(ms, data) {
        console.log('Starting task:', data, ms);
        return new Promise(resolve => setTimeout(resolve, ms, data));
    }
    
    var tasks = [
        async () => {
            var result = await wait(1000, 'moose');
            // do something with result
            console.log(result);
        },
        async () => {
            var result = await wait(500, 'taco');
            // do something with result
            console.log(result);
        },
        async () => {
            var result = await wait(5000, 'burp');
            // do something with result
            console.log(result);
        }
    ]
    
    await Promise.all(tasks.map(p => p()));
    console.log('done');
    

    然后输出:

    Starting task: moose 1000
    Starting task: taco 500
    Starting task: burp 5000
    taco
    moose
    burp
    done
    

    【讨论】:

      【解决方案7】:
          // A generic test function that can be configured 
          // with an arbitrary delay and to either resolve or reject
          const test = (delay, resolveSuccessfully) => new Promise((resolve, reject) => setTimeout(() => {
              console.log(`Done ${ delay }`);
              resolveSuccessfully ? resolve(`Resolved ${ delay }`) : reject(`Reject ${ delay }`)
          }, delay));
      
          // Our async handler function
          const handler = async () => {
              // Promise 1 runs first, but resolves last
              const p1 = test(10000, true);
              // Promise 2 run second, and also resolves
              const p2 = test(5000, true);
              // Promise 3 runs last, but completes first (with a rejection) 
              // Note the catch to trap the error immediately
              const p3 = test(1000, false).catch(e => console.log(e));
              // Await all in parallel
              const r = await Promise.all([p1, p2, p3]);
              // Display the results
              console.log(r);
          };
      
          // Run the handler
          handler();
          /*
          Done 1000
          Reject 1000
          Done 5000
          Done 10000
          */
      

      虽然设置 p1、p2 和 p3 并不是严格地并行运行它们,但它们不会阻止任何执行,您可以使用 catch 捕获上下文错误。

      【讨论】:

      • 欢迎来到 Stack Overflow。虽然您的代码可能会提供问题的答案,但请在其周围添加上下文,以便其他人了解它的作用以及它存在的原因。
      【解决方案8】:

      还有另一种没有 Promise.all() 的方法可以并行执行:

      首先,我们有两个打印数字的函数:

      function printNumber1() {
         return new Promise((resolve,reject) => {
            setTimeout(() => {
            console.log("Number1 is done");
            resolve(10);
            },1000);
         });
      }
      
      function printNumber2() {
         return new Promise((resolve,reject) => {
            setTimeout(() => {
            console.log("Number2 is done");
            resolve(20);
            },500);
         });
      }
      

      这是顺序的:

      async function oneByOne() {
         const number1 = await printNumber1();
         const number2 = await printNumber2();
      } 
      //Output: Number1 is done, Number2 is done
      

      这是并行的:

      async function inParallel() {
         const promise1 = printNumber1();
         const promise2 = printNumber2();
         const number1 = await promise1;
         const number2 = await promise2;
      }
      //Output: Number2 is done, Number1 is done
      

      【讨论】:

      • 这很危险,promise2 可能会在 promise1 解决之前拒绝。如果发生这种情况,您将无法从 promise1 中捕获错误。在此答案中使用顺序模式,或使用Promise.all([printNumber1(), printNumber2()])
      • 你不能处理调用异步函数的错误吗?对我来说,这似乎更容易将.catch 单独添加到每个事物然后Promise.all 上面的答案
      【解决方案9】:

      我创建了一个辅助函数waitAll,也许它可以让它更甜。 它目前仅在 nodejs 中有效,在浏览器 chrome 中

          //const parallel = async (...items) => {
          const waitAll = async (...items) => {
              //this function does start execution the functions
              //the execution has been started before running this code here
              //instead it collects of the result of execution of the functions
      
              const temp = [];
              for (const item of items) {
                  //this is not
                  //temp.push(await item())
                  //it does wait for the result in series (not in parallel), but
                  //it doesn't affect the parallel execution of those functions
                  //because they haven started earlier
                  temp.push(await item);
              }
              return temp;
          };
      
          //the async functions are executed in parallel before passed
          //in the waitAll function
      
          //const finalResult = await waitAll(someResult(), anotherResult());
          //const finalResult = await parallel(someResult(), anotherResult());
          //or
          const [result1, result2] = await waitAll(someResult(), anotherResult());
          //const [result1, result2] = await parallel(someResult(), anotherResult());
      
      

      【讨论】:

      • 不,这里根本没有发生并行化。 for 循环依次等待每个 promise 并将结果添加到数组中。
      • 我知道这似乎不适合人们。所以我在 node.js 和浏览器中进行了测试。测试在 node.js (v10, v11)、firefox 中通过,它在浏览器 chrome 中不起作用。测试用例在gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
      • 我拒绝相信这一点。标准中没有说明 for 循环的不同迭代可以自动并行化。这不是javascript的工作方式。编写循环代码的方式,它意味着:“等待一个项目(await expr),然后将结果推送到临时,然后获取下一个项目(for循环的下一次迭代)。”对于每个项目的“等待”完全局限于循环的一次迭代。如果测试表明存在并行化,那一定是因为转译器正在做一些非标准的事情或者完全是错误的。
      • @SzczepanHołyszewski 您在不运行测试用例的情况下不信任的信心激励我进行一些重命名重构和额外的 cmets。所有代码都是普通的旧 ES6,不需要转译。
      • 不知道为什么这被如此强烈地反对。这与@user2883596 给出的答案基本相同。
      【解决方案10】:

      更新:

      原始答案使正确处理承诺拒绝变得困难(并且在某些情况下是不可能的)。正确的解决方法是使用Promise.all:

      const [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);
      

      原答案:

      请确保在等待任一函数之前调用这两个函数:

      // Call both functions
      const somePromise = someCall();
      const anotherPromise = anotherCall();
      
      // Await both promises    
      const someResult = await somePromise;
      const anotherResult = await anotherPromise;
      

      【讨论】:

      • 我会在代码中添加一个 cmets,因为没有什么说下一个开发人员会理解你在做什么 OOB。
      • 我觉得这肯定是最纯粹的答案
      • 这个答案比 Haven 的要清楚得多。很明显,函数调用将返回 promise 对象,然后await 会将它们解析为实际值。
      • 这似乎是粗略的一瞥,但has horrible problems with unhandled rejections不要使用这个!
      • @Bergi 你是对的,感谢您指出这一点!我已经用更好的解决方案更新了答案。
      【解决方案11】:

      TL;DR

      使用Promise.all进行并行函数调用,错误发生时应答行为不正确。


      首先,一次性执行所有个异步调用,获取所有Promise对象。其次,在Promise 对象上使用await。这样,当您等待第一个 Promise 解决其他异步调用时仍在进行中。总的来说,您只会等待最慢的异步调用。例如:

      // Begin first call and store promise without waiting
      const someResult = someCall();
      
      // Begin second call and store promise without waiting
      const anotherResult = anotherCall();
      
      // Now we await for both results, whose async processes have already been started
      const finalResult = [await someResult, await anotherResult];
      
      // At this point all calls have been resolved
      // Now when accessing someResult| anotherResult,
      // you will have a value instead of a promise
      

      JSbin 示例:http://jsbin.com/xerifanima/edit?js,console

      警告:await 调用是在同一行还是在不同行都没有关系,只要第一个 await 调用发生在之后 所有的异步调用。请参阅 JohnnyHK 的评论。


      更新:根据@bergi's answer,此答案在错误处理中的时间不同,它确实在错误发生时抛出错误,但毕竟承诺被执行。 我将结果与@jonny 的提示进行比较:[result1, result2] = Promise.all([async1(), async2()]),检查以下代码 sn-p

      const correctAsync500ms = () => {
        return new Promise(resolve => {
          setTimeout(resolve, 500, 'correct500msResult');
        });
      };
      
      const correctAsync100ms = () => {
        return new Promise(resolve => {
          setTimeout(resolve, 100, 'correct100msResult');
        });
      };
      
      const rejectAsync100ms = () => {
        return new Promise((resolve, reject) => {
          setTimeout(reject, 100, 'reject100msError');
        });
      };
      
      const asyncInArray = async (fun1, fun2) => {
        const label = 'test async functions in array';
        try {
          console.time(label);
          const p1 = fun1();
          const p2 = fun2();
          const result = [await p1, await p2];
          console.timeEnd(label);
        } catch (e) {
          console.error('error is', e);
          console.timeEnd(label);
        }
      };
      
      const asyncInPromiseAll = async (fun1, fun2) => {
        const label = 'test async functions with Promise.all';
        try {
          console.time(label);
          let [value1, value2] = await Promise.all([fun1(), fun2()]);
          console.timeEnd(label);
        } catch (e) {
          console.error('error is', e);
          console.timeEnd(label);
        }
      };
      
      (async () => {
        console.group('async functions without error');
        console.log('async functions without error: start')
        await asyncInArray(correctAsync500ms, correctAsync100ms);
        await asyncInPromiseAll(correctAsync500ms, correctAsync100ms);
        console.groupEnd();
      
        console.group('async functions with error');
        console.log('async functions with error: start')
        await asyncInArray(correctAsync500ms, rejectAsync100ms);
        await asyncInPromiseAll(correctAsync500ms, rejectAsync100ms);
        console.groupEnd();
      })();

      【讨论】:

      • 在我看来,这似乎是比 Promise.all 更好的选择——如果你将 const 更改为 let,通过解构赋值,你甚至可以执行 [someResult, anotherResult] = [await someResult, await anotherResult]
      • 但这仍然会连续执行await 语句,对吧?也就是说,执行会暂停,直到第一个 await 解决,然后移动到第二个。 Promise.all 并行执行。
      • 这个答案具有误导性,因为两个等待都在同一行中完成是无关紧要的。重要的是这两个异步调用是在等待之前进行的。
      • @Haven 这个解决方案和Promise.all 不一样。如果每个请求都是网络调用,则需要在启动 await anotherResult 之前解析 await someResult。相反,在Promise.all 中,两个await 调用可以在其中任何一个解决之前启动。
      • 这个答案是完全错误的,应该删除。
      【解决方案12】:

      我投票给:

      await Promise.all([someCall(), anotherCall()]);
      

      注意你调用函数的那一刻,它可能会导致意想不到的结果:

      // Supposing anotherCall() will trigger a request to create a new User
      
      if (callFirst) {
        await someCall();
      } else {
        await Promise.all([someCall(), anotherCall()]); // --> create new User here
      }
      

      但是关注总是触发创建新用户的请求

      // Supposing anotherCall() will trigger a request to create a new User
      
      const someResult = someCall();
      const anotherResult = anotherCall(); // ->> This always creates new User
      
      if (callFirst) {
        await someCall();
      } else {
        const finalResult = [await someResult, await anotherResult]
      }
      

      【讨论】:

      • 因为您在条件测试之外/之前声明了函数,并调用了它们。尝试将它们包装在 else 块中。
      • @Haven:我的意思是,当您将 call 函数与 await 分开的时刻可能会导致意想不到的结果,例如:异步 HTTP 请求。
      猜你喜欢
      • 1970-01-01
      • 2019-02-17
      • 2021-03-15
      • 1970-01-01
      • 1970-01-01
      • 2021-08-27
      相关资源
      最近更新 更多