Wywołaj funkcje asynchroniczne / oczekujące równolegle

431

O ile rozumiem, w ES7 / ES2016 umieszczanie wielu awaitw kodzie będzie działać podobnie do łączenia łańcuchowego .then()z obietnicami, co oznacza, że ​​będą one wykonywać jeden po drugim, a nie równolegle. Na przykład mamy ten kod:

await someCall();
await anotherCall();

Czy rozumiem to poprawnie, że anotherCall()zostanie wywołane dopiero po someCall()zakończeniu? Jaki jest najbardziej elegancki sposób nazywania ich równolegle?

Chcę go używać w węźle, więc może istnieje rozwiązanie z biblioteką asynchroniczną?

EDYCJA: Nie jestem zadowolony z rozwiązania przedstawionego w tym pytaniu: Spowolnienie z powodu nierównoległego oczekiwania na obietnice w generatorach asynchronicznych , ponieważ używa generatorów i pytam o bardziej ogólny przypadek użycia.

Victor Marchuk
źródło
1
@ adeneo Niepoprawne, JavaScript nigdy nie działa równolegle we własnym kontekście.
Blindman67
5
@ Blindman67 - działa przynajmniej tak, jak OP oznacza, że ​​dwie operacje asynchroniczne działają jednocześnie, ale nie w tym przypadku chciałem napisać, że działają one szeregowo , pierwsza awaitczekałaby na zakończenie pierwszej funkcji całkowicie przed wykonaniem drugiego.
adeneo
3
@ Blindman67 - jest jednowątkowy, ale to ograniczenie nie dotyczy metod asynchronicznych, mogą one działać jednocześnie i zwracać odpowiedź po ich zakończeniu, tj. Co OP rozumie przez „równoległe”.
adeneo
7
@ Blindman67 - Myślę, że jest całkiem jasne, o co prosi OP, użycie wzorca asynchronicznego / oczekującego sprawi, że funkcje będą działały szeregowo, nawet jeśli są asynchroniczne, więc pierwszy zakończy się całkowicie przed wywołaniem drugiego itd. OP jest pytając, jak wywołać obie funkcje w trybie równoległym, a ponieważ są one wyraźnie asynchroniczne, celem jest ich jednoczesne uruchomienie, tj. w trybie równoległym, na przykład wykonanie dwóch żądań ajax jednocześnie, co nie jest żadnym problemem w javascript, ponieważ większość metod asynchronicznych , jak zauważyłeś, uruchamia kod macierzysty i używa więcej wątków.
adeneo
3
@Bergi nie jest to duplikat połączonego pytania - dotyczy to w szczególności składni asynchronicznej / oczekującej i natywnych Promises. Połączone pytanie dotyczy biblioteki bluebird z generatorami i wydajnością. Być może podobne koncepcyjnie, ale nie w realizacji.
iest

Odpowiedzi:

700

Możesz poczekać na Promise.all():

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

Aby zapisać wyniki:

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

Zauważ, że Promise.allszybko się nie udaje, co oznacza, że ​​gdy tylko jedna z podanych mu obietnic odrzuci, wtedy cała rzecz odrzuci.

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'

Jeśli zamiast tego chcesz poczekać na spełnienie lub odrzucenie wszystkich obietnic, możesz skorzystać Promise.allSettled. Zauważ, że Internet Explorer nie obsługuje natywnie tej metody.

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

madox2
źródło
78
Oczyść, ale bądź świadomy szybkiego działania Promise.all. Jeśli którakolwiek z funkcji zgłosi błąd, Promise.all odrzuci
NoNameProvided
11
Możesz ładnie obsługiwać częściowe wyniki za pomocą asynchronizacji / oczekiwania, patrz stackoverflow.com/a/42158854/2019689
NoNameProvided
131
Pro wskazówka: użyj restrukturyzacji tablic, aby zainicjować dowolną liczbę wyników z Promise.all (), takich jak:[result1, result2] = Promise.all([async1(), async2()]);
jonny 10'18
10
@jonny Czy dotyczy to szybkiego działania? Czy nadal trzeba = await Promise.all?
theUtherSide
5
@ thetherther Masz całkowitą rację - zaniedbałem uwzględnienie oczekiwania.
jonny
114

TL; DR

Użyj Promise.alldla równoległych wywołań funkcji, zachowanie nie odpowiada poprawnie, gdy wystąpi błąd.


Najpierw wykonaj wszystkie asynchroniczne wywołania jednocześnie i uzyskaj wszystkie Promiseobiekty. Po drugie, użyj awaitna Promiseobiektach. W ten sposób podczas oczekiwania na Promiserozstrzygnięcie pozostałych asynchronicznych wywołań nadal postępuje. Ogólnie rzecz biorąc, będziesz czekać tylko tak długo, jak najwolniejsze połączenie asynchroniczne. Na przykład:

// 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

Przykład JSbin: http://jsbin.com/xerifanima/edit?js,console

Zastrzeżenie: Nie ma znaczenia, czy awaitpołączenia są na tej samej linii, czy na różnych liniach, o ile pierwsze awaitpołączenie następuje po wszystkich połączeniach asynchronicznych. Zobacz komentarz JohnnyHK.


Aktualizacja: ta odpowiedź ma inny czas obsługi błędów w zależności od odpowiedzi @ bergi , NIE wyrzuca błędu w momencie wystąpienia błędu, ale po wykonaniu wszystkich obietnic. Porównuję wynik z wskazówką @ jonny: [result1, result2] = Promise.all([async1(), async2()])sprawdź następujący fragment kodu

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

Przystań
źródło
11
To wygląda o wiele ładniejszy opcją dla mnie niż Promise.all - a wraz z destructuring assignment Możesz to zrobić nawet [someResult, anotherResult] = [await someResult, await anotherResult]jeśli zmieni constsię let.
jawj
28
Ale to wciąż wykonuje awaitpolecenia serio, prawda? Oznacza to, że wykonywanie jest wstrzymywane do momentu pierwszego awaitrozstrzygnięcia, a następnie przechodzi do drugiego. Promise.allwykonuje się równolegle.
Andru
8
Dziękuję @Haven. To powinna być zaakceptowana odpowiedź.
Stefan D
87
Ta odpowiedź jest myląca, ponieważ fakt, że oba oczekiwania są wykonywane w tej samej linii, nie ma znaczenia. Ważne jest to, że dwa połączenia asynchroniczne są wykonywane przed oczekiwaniem na jedno z nich.
JohnnyHK
15
@Haven to rozwiązanie nie jest takie samo jak Promise.all. Jeśli każde żądanie jest połączeniem sieciowym, await someResultnależy je rozwiązać, zanim await anotherResultjeszcze zostanie uruchomione. I odwrotnie, w Promise.allobu awaitpołączeniach można rozpocząć przed rozstrzygnięciem jednego z nich.
Ben Winding
89

Aktualizacja:

Oryginalna odpowiedź utrudnia (a w niektórych przypadkach uniemożliwia) prawidłową obsługę odrzuconych obietnic. Prawidłowym rozwiązaniem jest użycie Promise.all:

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

Oryginalna odpowiedź:

Upewnij się tylko, że wywołujesz obie funkcje, zanim zaczekasz na jedną z nich:

// Call both functions
const somePromise = someCall();
const anotherPromise = anotherCall();

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
źródło
1
@JeffFischer Dodałem komentarze, które mam nadzieję, że wyjaśnią.
Jonathan Potter
9
Wydaje mi się, że jest to z pewnością najczystsza odpowiedź
Gershom
1
Ta odpowiedź jest znacznie bardziej wyraźna niż odpowiedź Haven. Oczywiste jest, że wywołania funkcji zwracają obiekty obiecane, a awaitnastępnie przekształcają je w rzeczywiste wartości.
user1032613,
3
Wydaje się, że działa to pobieżnie, ale ma okropne problemy z nieobsługiwanymi odrzuceniami . Nie używaj tego!
Bergi,
1
@Bergi Masz rację, dziękuję za zwrócenie na to uwagi! Zaktualizowałem odpowiedź, stosując lepsze rozwiązanie.
Jonathan Potter
24

Istnieje inny sposób bez Promise.all (), aby zrobić to równolegle:

Po pierwsze, mamy 2 funkcje do drukowania liczb:

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

To jest sekwencyjne:

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

Jest to równoległe:

async function inParallel() {
   const promise1 = printNumber1();
   const promise2 = printNumber2();
   const number1 = await promise1;
   const number2 = await promise2;
}
//Output: Number2 is done, Number1 is done
użytkownik 2883596
źródło
10

Można to osiągnąć za pomocą Promise.allSettled () , która jest podobna do Promise.all()zachowania bezawaryjnego, ale bez niego.

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!"}

Uwaga : jest to najnowocześniejsza funkcja z ograniczoną obsługą przeglądarki, dlatego zdecydowanie zalecam dodanie do tej funkcji polifill.

Jonathan Sudiaman
źródło
7

Stworzyłem istotę testującą różne sposoby rozwiązywania obietnic, z wynikami. Pomocne może być sprawdzenie działających opcji.

SkarXa
źródło
Testy 4 i 6 w gist zwróciły oczekiwane wyniki. Zobacz stackoverflow.com/a/42158854/5683904 autorstwa NoNameProvided, który wyjaśnia różnicę między opcjami.
akraines
1
    // 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
    */

Chociaż ustawienie p1, p2 i p3 nie jest ściśle uruchamiane równolegle, nie wstrzymują one żadnego wykonania i można złapać błędy kontekstowe za pomocą haczyka.

Thrunobulax
źródło
2
Witamy w Stack Overflow. Chociaż Twój kod może dostarczyć odpowiedzi na pytanie, dodaj kontekst wokół niego, aby inni mogli zorientować się, co robi i dlaczego.
Theo
1

W moim przypadku mam kilka zadań, które chcę wykonywać równolegle, ale muszę zrobić coś innego z wynikiem tych zadań.

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

A wynik:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
źródło
fajne dla dynamicznego tworzenia (szereg zasobów)
Michał Miky Jankovský
1

czekaj na Promise.all ([someCall (), anotherCall ()]); jak już wspomniano, będzie działać jak ogrodzenie wątku (bardzo powszechne w kodzie równoległym jako CUDA), dlatego pozwoli na działanie wszystkich zawartych w nim obietnic bez wzajemnego blokowania, ale uniemożliwi kontynuowanie wykonywania, dopóki WSZYSTKIE nie zostaną rozwiązane.

innym podejściem, które warto udostępnić, jest asynchronizacja Node.js, która pozwoli również łatwo kontrolować ilość współbieżności, która jest zwykle pożądana, jeśli zadanie jest bezpośrednio powiązane z wykorzystaniem ograniczonych zasobów jako wywołania API, operacji we / wy, itp.

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

Podziękowania dla autora artykułu Medium ( czytaj więcej )

Thiago Conrado
źródło
-5

Głosuję za:

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

Pamiętaj o momencie, w którym wywołujesz funkcje, może to spowodować nieoczekiwany rezultat:

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

Ale przestrzeganie zawsze powoduje żądanie utworzenia nowego użytkownika

// 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]
}
Hoang Le Anh Tu
źródło
Ponieważ deklarujesz funkcję na zewnątrz / przed testem warunków i wywołujesz je. Spróbuj owinąć je elseblokami.
Haven
@ Przystań: Mam na myśli, że oddzielenie momentów, w których wywołujesz funkcje, a oczekiwanie, może prowadzić do nieoczekiwanych rezultatów, na przykład: asynchroniczne żądania HTTP.
Hoang Le Anh Tu
-6

Tworzę funkcję pomocnika waitAll, może sprawi, że będzie słodsza. Na razie działa tylko w nodejs , a nie w przeglądarce 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());
Fred Yang
źródło
3
Nie, równoległość wcale się tutaj nie dzieje. forPętla kolejno czeka każdą obietnicę i dodaje wynik do tablicy.
Szczepan Hołyszewski
Rozumiem, że to nie działa na ludzi. Więc przetestowałem w node.js i przeglądarce. Test jest przekazywany w node.js (v10, v11), Firefox, nie działa w przeglądarce Chrome. Przypadek testowy znajduje się w gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang
2
Nie wierzę w to. W normie nie ma nic, co mówi, że różne iteracje pętli for mogą być automagicznie równoległe; nie tak działa JavaScript. Sposób, w jaki kod pętli jest zapisany, oznacza to: „czekaj na jeden element (czekaj na wyrażenie), NASTĘPNIE wypchnij wynik do temp, NASTĘPNIE weź następny element (następna iteracja pętli for).„ Oczekiwanie ”na każdy element jest całkowicie ogranicza się do pojedynczej iteracji pętli. Jeśli testy wykazują, że występuje równoległość, musi to być spowodowane tym, że transpiler robi coś niestandardowego lub jest całkowicie
błędny
@ SzczepanHołyszewski Twoje zaufanie do zniechęcania bez uruchamiania przypadku testowego zainspirowało mnie do zmiany nazwy na refail i dodatkowe komentarze. Cały kod jest zwykłym starym ES6, transpilacja nie jest wymagana.
Fred Yang
Nie jestem pewien, dlaczego jest to tak mocno oceniane. Jest to w zasadzie ta sama odpowiedź, którą udzielił @ user2883596.
Jonathan Sudiaman