簡體   English   中英

並行調用 async/await 函數

[英]Call async/await functions in parallel

據我了解,在 ES7/ES2016 中,在代碼中放置多個await的工作方式類似於將.then()與 Promise 鏈接起來,這意味着它們將一個接一個地執行而不是並行執行。 因此,例如,我們有以下代碼:

await someCall();
await anotherCall();

我是否正確理解anotherCall()只有在someCall()完成時才會被調用? 並行調用它們的最優雅方式是什么?

我想在 Node 中使用它,所以也許有異步庫的解決方案?

編輯:我對這個問題中提供的解決方案不滿意: Slowdown due to non-parallel waiting of promises in async generators ,因為它使用了生成器,我在問一個更一般的用例。

你可以在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同時它會提供哪些操作失敗和哪些操作失敗的確切細節。

TL; 博士

使用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的答案,如發生錯誤時不會拋出了錯誤,但所有的承諾都執行之后。 我將結果與[result1, result2] = Promise.all([async1(), async2()])的提示進行比較: [result1, result2] = Promise.all([async1(), async2()]) ,檢查以下代碼片段

 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 [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;

還有另一種沒有 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

我創建了一個 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 }
*/

就我而言,我有幾個要並行執行的任務,但我需要對這些任務的結果做一些不同的事情。

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

直觀的解決方案

 function wait(ms, data) { console.log('Starting task:', data, ms); return new Promise( resolve => setTimeout(resolve, ms, data) ); } (async function parallel() { // step 1 - initiate all promises console.log('STARTING') let task1 = wait(2000, 'parallelTask1') // PS: see Exception handling below let task2 = wait(500, 'parallelTask2') let task3 = wait(1000, 'parallelTask3') // step 2 - await all promises console.log('WAITING') task1 = await task1 task2 = await task2 task3 = await task3 // step 3 - all results are 100% ready console.log('FINISHED') console.log('Result:', task1, task2, task3) })() 

  1. 這將一個接一個地執行承諾,但立即執行,它們將繼續同時運行。
  2. 這是我們暫停進一步的代碼執行並等待它們完成的地方。 順序無關緊要,先解決的是哪一個。 在所有代碼都解決之前,該代碼將不會繼續執行步驟3。 如果第一個使用的時間最長,則不必再等待第二個,因為在代碼到達那里之前它已經完成了。
  3. 完成,最后的承諾已解決,最后await調用完成了代碼執行。


使用ES6,您甚至可以在執行啟動后的第2步中執行此操作

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


PS:您也可以等待內部計算

 let five = getAsyncFive() let ten = getAsyncTen() let result = await five * await ten 

*請注意,它與let result = await getAsyncFive() * await getAsyncTen()因為這不會並行運行異步任務!


異常處理

在下面的代碼段中, .catch(e => e)捕獲錯誤並允許鏈繼續進行,從而使promise得以解決而不是拒絕 如果沒有catch ,則代碼將引發未處理的異常,並且該函數將提前退出。

 const wait = (ms, data) => log(ms,data) || new Promise( resolve => setTimeout(resolve, ms, data) ) const reject = (ms, data) => log(ms,data) || new Promise( (r, reject) => setTimeout(reject, ms, data) ) const e = e => 'err-' + e const l = l => (console.log('Done:', l), l) const log = (ms, data) => console.log('Started', data, ms) ;(async function parallel() { let task1 = reject(500, 'parallelTask1').catch(e).then(l) let task2 = wait(2000, 'parallelTask2').catch(e).then(l) let task3 = reject(1000, 'parallelTask3').catch(e).then(l) console.log('WAITING') task1 = await task1 task2 = await task2 task3 = await task3 console.log('FINISHED', task1, task2, task3) })() 

第二個代碼段未處理,功能將失敗。
您也可以打開Devtools並在控制台輸出中查看錯誤。

 const wait = (ms, data) => log(ms,data) || new Promise( resolve => setTimeout(resolve, ms, data) ) const reject = (ms, data) => log(ms,data) || new Promise( (r, reject) => setTimeout(reject, ms, data) ) const e = e => 'err-' + e const l = l => (console.log('Done:', l), l) const log = (ms, data) => console.log('Started', data, ms) console.log('here1') ;(async function parallel() { let task1 = reject(500, 'parallelTask1').then(l) // catch is removed let task2 = wait(2000, 'parallelTask2').then(l) let task3 = reject(1000, 'parallelTask3').then(l) console.log('WAITING') task1 = await task1 task2 = await task2 task3 = await task3 console.log('FINISHED', task1, task2, task3) })() console.log('here2') // Note: "FINISHED" will not run 

等待 Promise.all([someCall(), anotherCall()]); 如前所述,它將充當線程柵欄(在作為 CUDA 的並行代碼中非常常見),因此它將允許其中的所有 promise 運行而不會相互阻塞,但會阻止執行繼續,直到解決所有問題。

另一種值得分享的方法是 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 文章作者( 閱讀更多

    // 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 捕獲上下文錯誤。

您可以調用多個異步函數而無需等待它們。 這將並行執行它們。 這樣做時,將返回的承諾保存在變量中,並在某個時候單獨或使用 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()

這可以通過Promise.allSettled()來實現,它類似於Promise.all()但沒有快速失敗行為。

async function failure() {
    throw "Failure!";
}

async function success() {
    return "Success!";
}

const [failureResult, successResult] = await Promise.allSettled([failure(), success()]);

console.log(failureResult); // {status: "rejected", reason: "Failure!"}
console.log(successResult); // {status: "fulfilled", value: "Success!"}

注意 :這是一項最新功能,瀏覽器支持有限,因此,我強烈建議為此功能包括一個polyfill。

這可以通過Promise.allSettled()來完成,它類似於Promise.all()但沒有快速故障行為。

async function Promise1() {
    throw "Failure!";
}

async function Promise2() {
    return "Success!";
}

const [Promise1Result, Promise2Result] = await Promise.allSettled([Promise1(), Promise2()]);

console.log(Promise1Result); // {status: "rejected", reason: "Failure!"}
console.log(Promise2Result); // {status: "fulfilled", value: "Success!"}

注意:這是一個有限瀏覽器支持的前沿特性,所以我強烈建議為這個 function 包含一個 polyfill。

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});
            })
    }
}

我創建了一個輔助函數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());

我投票贊成:

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]
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM