Czy Node.js natywny Promise.all przetwarza równolegle czy sekwencyjnie?

173

Chciałbym to wyjaśnić, ponieważ dokumentacja nie jest na ten temat zbyt jasna;

P1: Czy Promise.all(iterable)wszystkie obietnice są przetwarzane sekwencyjnie czy równolegle? A dokładniej, jest to odpowiednik wykonywania obietnic łańcuchowych, takich jak

p1.then(p2).then(p3).then(p4).then(p5)....

czy jest jakiś inny rodzaj algorytmu, gdzie wszystko p1, p2, p3, p4, p5, itd. są nazywane jednocześnie (równolegle) i wyniki są zwracane najszybciej jak wszystkim determinacji (lub jeden odrzutów)?

P2: Jeśli Promise.alldziała równolegle, czy istnieje wygodny sposób na sekwencyjne uruchamianie iterowalnych?

Uwaga : nie chcę używać Q ani Bluebird, ale wszystkie natywne specyfikacje ES6.

Yanick Rochon
źródło
Czy pytasz o implementację węzła (V8) lub o specyfikację?
Amit
1
Jestem prawie pewien, że Promise.allwykonuje je równolegle.
royhowie
@Amit I oflagowałem node.jsi io.jsjak tu go używam. Więc tak, implementacja V8, jeśli wolisz.
Yanick Rochon
9
Obietnic nie można „spełnić”. Rozpoczynają swoje zadanie, gdy są tworzone - przedstawiają tylko wyniki - a Ty wykonujesz wszystko równolegle, nawet przed przekazaniem ich do Promise.all.
Bergi
Obietnice są spełniane w momencie stworzenia. (można to potwierdzić, uruchamiając kawałek kodu). W new Promise(a).then(b); c();a jest wykonywane najpierw, potem c, potem b. To nie obietnica. Wszystko, co realizuje te obietnice, po prostu obsługuje, gdy się rozwiążą.
Mateon1

Odpowiedzi:

257

Czy Promise.all(iterable)dotrzymujesz wszystkich obietnic?

Nie, obietnic nie można „zrealizować”. Rozpoczynają swoje zadanie, gdy są tworzone - przedstawiają tylko wyniki - a Ty wykonujesz wszystko równolegle, nawet przed przekazaniem ich do Promise.all.

Promise.allczeka tylko na wiele obietnic. Nie ma znaczenia, w jakiej kolejności rozwiązują, ani czy obliczenia są wykonywane równolegle.

czy istnieje wygodny sposób na sekwencyjne uruchamianie iterowalnego?

Jeśli masz już swoje obietnice, nie możesz wiele zrobić, ale Promise.all([p1, p2, p3, …])(co nie ma pojęcia kolejności). Ale jeśli masz iterowalne funkcje asynchroniczne, możesz rzeczywiście uruchamiać je sekwencyjnie. Zasadniczo musisz wyjść z

[fn1, fn2, fn3, …]

do

fn1().then(fn2).then(fn3).then(…)

a rozwiązaniem jest użycie Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())
Bergi
źródło
1
Czy w tym przykładzie jest iterowalna tablica funkcji, które zwracają obietnicę, którą chcesz wywołać?
James Reategui
2
@SSHThis: Jest dokładnie tak, jak thensekwencja - wartość zwracana jest obietnicą dla ostatniego fnwyniku i możesz łączyć inne wywołania zwrotne z tym.
Bergi
1
@wojjas To dokładnie odpowiednik fn1().then(p2).then(fn3).catch(…? Nie ma potrzeby używania wyrażenia funkcyjnego.
Bergi
1
@wojjas Oczywiście, że retValFromF1jest przekazywane p2, to jest dokładnie to, co p2robi. Oczywiście, jeśli chcesz zrobić więcej (przekazać dodatkowe zmienne, wywołać wiele funkcji itp.), Musisz użyć wyrażenia funkcyjnego, chociaż zmiana p2w tablicy byłaby łatwiejsza
Bergi
1
@ robe007 Tak, chodziło mi iterableo [fn1, fn2, fn3, …]tablicę
Bergi
62

Równolegle

await Promise.all(items.map(async item => { await fetchItem(item) }))

Zalety: Szybciej. Wszystkie iteracje zostaną wykonane, nawet jeśli jedna się nie powiedzie.

Kolejno

for (let i = 0; i < items.length; i++) {
    await fetchItem(items[i])
}

Zalety: Zmienne w pętli mogą być współużytkowane przez każdą iterację. Zachowuje się jak normalny imperatywny kod synchroniczny.

david_adler
źródło
7
Lub:for (const item of items) await fetchItem(item);
Robert Penner
1
@david_adler W równoległym przykładzie zalety, o których powiedziałeś, że wszystkie iteracje zostaną wykonane, nawet jeśli jedna się nie powiedzie . Jeśli się nie mylę, to i tak szybko się nie powiedzie. Aby zmienić to zachowanie, można zrobić coś takiego: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor,
@Taimoor tak, działa szybko i kontynuuje wykonywanie kodu po Promise.all, ale wszystkie iteracje są nadal wykonywane codepen.io/mfbx9da4/pen/BbaaXr
david_adler
Takie podejście jest lepsze, gdy asyncfunkcja jest wywołaniem API i nie chcesz DDOS na serwerze. Masz lepszą kontrolę nad poszczególnymi wynikami i błędami zgłaszanymi podczas wykonywania. Jeszcze lepiej możesz zdecydować, które błędy kontynuować i co przerwać pętlę.
mandarynka
Zauważ, że javascript w rzeczywistości nie wykonuje asynchronicznych żądań „równolegle” przy użyciu wątków, ponieważ javascript jest jednowątkowy. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler
11

Odpowiedź Bergisa sprawiła, że ​​znalazłem się na właściwej drodze, używając Array.reduce.

Jednak, aby faktycznie uzyskać funkcje zwracające moje obietnice wykonania jedna po drugiej, musiałem dodać więcej zagnieżdżenia.

Mój prawdziwy przypadek użycia to tablica plików, które muszę przesłać w kolejności jeden po drugim ze względu na ograniczenia w dół ...

Oto, z czym skończyłem.

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(() => {
            return transferFile(theFile); //function returns a promise
        });
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

Jak sugerują poprzednie odpowiedzi, użyj:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(transferFile(theFile));
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

nie czekał na zakończenie przesyłania przed rozpoczęciem kolejnego, a tekst „Wszystkie przesłane pliki” pojawił się jeszcze przed rozpoczęciem przesyłania pierwszego pliku.

Nie jestem pewien, co zrobiłem źle, ale chciałem się podzielić tym, co zadziałało.

Edycja: Odkąd napisałem ten post, teraz rozumiem, dlaczego pierwsza wersja nie działała. then () oczekuje funkcji zwracającej obietnicę. Więc powinieneś podać nazwę funkcji bez nawiasów! Teraz moja funkcja potrzebuje argumentu, więc muszę zawinąć w anonimową funkcję nie pobierającą żadnego argumentu!

tkarls
źródło
4

tylko po to, aby rozwinąć odpowiedź @ Bergi (która jest bardzo zwięzła, ale trudna do zrozumienia;)

Ten kod uruchomi każdy element w tablicy i doda następny łańcuch „then chain” na końcu;

function eachorder(prev,order) {
        return prev.then(function() {
          return get_order(order)
            .then(check_order)
            .then(update_order);
        });
    }
orderArray.reduce(eachorder,Promise.resolve());

mam nadzieję, że to ma sens.

TimoSolo
źródło
3

Możesz również przetwarzać iterowalne sekwencyjnie z funkcją asynchroniczną, używając funkcji rekurencyjnej. Na przykład biorąc pod uwagę tablicę ado przetworzenia z funkcją asynchroniczną someAsyncFunction():

var a = [1, 2, 3, 4, 5, 6]

function someAsyncFunction(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("someAsyncFunction: ", n)
      resolve(n)
    }, Math.random() * 1500)
  })
}

//You can run each array sequentially with: 

function sequential(arr, index = 0) {
  if (index >= arr.length) return Promise.resolve()
  return someAsyncFunction(arr[index])
    .then(r => {
      console.log("got value: ", r)
      return sequential(arr, index + 1)
    })
}

sequential(a).then(() => console.log("done"))

Mark Meyer
źródło
użycie array.prototype.reducejest znacznie lepsze pod względem wydajności niż funkcja rekurencyjna
Mateusz Sowiński
@ MateuszSowiński, pomiędzy każdym połączeniem jest limit czasu 1500ms. Biorąc pod uwagę, że jest to wykonywanie wywołań asynchronicznych sekwencyjnie, trudno jest zrozumieć, jak to ma znaczenie, nawet w przypadku bardzo szybkiej zmiany asynchronicznej.
Mark Meyer
Powiedzmy, że musisz wykonać po sobie 40 naprawdę szybkich funkcji asynchronicznych - używanie funkcji rekurencyjnych dość szybko zapychałoby pamięć
Mateusz Sowiński
@ MateuszSowiński, że stack się tu nie kończy ... wracamy po każdym sprawdzeniu. Porównaj to z sytuacją, w reducektórej musisz zbudować cały then()łańcuch w jednym kroku, a następnie wykonać.
Mark Meyer
W 40 wywołaniu funkcji sekwencyjnej pierwsze wywołanie funkcji wciąż jest w pamięci, czekając na powrót łańcucha funkcji sekwencyjnych
Mateusz Sowiński
2

Za pomocą async await tablicę obietnic można łatwo wykonać sekwencyjnie:

let a = [promise1, promise2, promise3];

async function func() {
  for(let i=0; i<a.length; i++){
    await a[i]();
  }  
}

func();

Uwaga: W powyższej implementacji, jeśli obietnica zostanie odrzucona, reszta nie zostanie wykonana.Jeśli chcesz, aby wszystkie obietnice zostały wykonane, zawiń swoje await a[i]();wnętrzetry catch

Ayan
źródło
2

równolegle

zobacz ten przykład

const resolveAfterTimeout = async i => {
  return new Promise(resolve => {
    console.log("CALLED");
    setTimeout(() => {
      resolve("RESOLVED", i);
    }, 5000);
  });
};

const call = async () => {
  const res = await Promise.all([
    resolveAfterTimeout(1),
    resolveAfterTimeout(2),
    resolveAfterTimeout(3),
    resolveAfterTimeout(4),
    resolveAfterTimeout(5),
    resolveAfterTimeout(6)
  ]);
  console.log({ res });
};

call();

uruchamiając kod, będzie konsolidował "CALLED" dla wszystkich sześciu obietnic, a kiedy zostaną rozwiązane, będzie konsolidował co 6 odpowiedzi po przekroczeniu limitu czasu w tym samym czasie

Chintan Rajpara
źródło
2

NodeJS nie uruchamia obietnic równolegle, uruchamia je jednocześnie, ponieważ jest to architektura pojedynczej pętli zdarzeń. Istnieje możliwość równoległego uruchamiania, tworząc nowy proces potomny w celu wykorzystania wielordzeniowego procesora.

Równoległe kontra współbieżne

W rzeczywistości Promise.alljest to zestawianie funkcji obietnic w odpowiedniej kolejce (patrz architektura pętli zdarzeń), uruchamianie ich jednocześnie (wywołanie P1, P2, ...), a następnie czekanie na każdy wynik, a następnie rozwiązanie Promise. Wszystko ze wszystkimi obietnicami wyniki. Obietnica. Wszystko zawiedzie przy pierwszej obietnicy, która zawodzi, chyba że sam poradzisz sobie z odrzuceniem.

Istnieje zasadnicza różnica między równoległym i współbieżnym, pierwszy z nich będzie wykonywał różne obliczenia w oddzielnym procesie dokładnie w tym samym czasie i będą postępować w tym rytmie, podczas gdy drugi wykona różne obliczenia jeden po drugim, nie czekając na poprzednie obliczenia, aby zakończyć i postępować w tym samym czasie, bez uzależnienia od siebie nawzajem.

Wreszcie, aby odpowiedzieć na twoje pytanie, Promise.allnie zostanie wykonany ani równolegle, ani sekwencyjnie, ale równolegle.

Adrien De Peretti
źródło
To nie jest w porządku. NodeJS może uruchamiać rzeczy równolegle. NodeJS ma koncepcję wątku roboczego. Domyślnie liczba wątków roboczych wynosi 4. Na przykład, jeśli używasz biblioteki kryptograficznej do haszowania dwóch wartości, możesz wykonywać je równolegle. Zadanie wykonają dwa wątki robocze. Oczywiście twój procesor musi być wielordzeniowy, aby obsługiwał równoległość.
Shihab
Tak, masz rację, to właśnie powiedziałem na końcu pierwszego akapitu, ale mówiłem o procesie dziecięcym, oczywiście, że mogą kierować pracownikami.
Adrien De Peretti
1

Odpowiedź Bergiego pomogła mi uczynić wywołanie synchronicznym.Dodałem poniżej przykład, w którym wywołujemy każdą funkcję po wywołaniu poprzedniej funkcji.

function func1 (param1) {
    console.log("function1 : " + param1);
}
function func2 () {
    console.log("function2");
}
function func3 (param2, param3) {
    console.log("function3 : " + param2 + ", " + param3);
}

function func4 (param4) {
    console.log("function4 : " + param4);
}
param4 = "Kate";

//adding 3 functions to array

a=[
    ()=>func1("Hi"),
    ()=>func2(),
    ()=>func3("Lindsay",param4)
  ];

//adding 4th function

a.push(()=>func4("dad"));

//below does func1().then(func2).then(func3).then(func4)

a.reduce((p, fn) => p.then(fn), Promise.resolve());
Nithi
źródło
Czy to odpowiedź na pierwotne pytanie?
Giulio Caccin
0

Możesz to zrobić za pomocą pętli for.

obietnica zwrotu funkcji async

async function createClient(client) {
    return await Client.create(client);
}

let clients = [client1, client2, client3];

jeśli napiszesz następujący kod, klient zostanie utworzony równolegle

const createdClientsArray = yield Promise.all(clients.map((client) =>
    createClient(client);
));

wtedy wszyscy klienci są tworzeni równolegle. ale jeśli chcesz stworzyć klienta sekwencyjnie, powinieneś użyć pętli for

const createdClientsArray = [];
for(let i = 0; i < clients.length; i++) {
    const createdClient = yield createClient(clients[i]);
    createdClientsArray.push(createdClient);
}

następnie wszyscy klienci są tworzeni sekwencyjnie.

miłego kodowania :)

Deepak Sisodiya
źródło
8
W tej chwili async/ awaitjest dostępne tylko z transpilerem lub przy użyciu innych silników niż Node. Poza tym naprawdę nie powinieneś mieszać asyncz yield. Chociaż zachowują się tak samo z transpilerem i conaprawdę są zupełnie inne i zwykle nie powinny się wzajemnie zastępować. Powinieneś także wspomnieć o tych ograniczeniach, ponieważ twoja odpowiedź jest myląca dla początkujących programistów.
Yanick Rochon
0

Używałem for do rozwiązywania kolejnych obietnic. Nie jestem pewien, czy to pomaga, ale właśnie to robiłem.

async function run() {
    for (let val of arr) {
        const res = await someQuery(val)
        console.log(val)
    }
}

run().then().catch()
Nick Kotenberg
źródło
0

to może odpowiedzieć na część twojego pytania.

tak, możesz połączyć tablicę funkcji zwracających obietnice w następujący sposób ... (spowoduje to przekazanie wyniku każdej funkcji do następnej). możesz oczywiście edytować go, aby przekazać ten sam argument (lub żadnych argumentów) do każdej funkcji.

function tester1(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a + 1);
    }, 1000);
  })
}

function tester2(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a * 5);
    }, 1000);
  })
}

function promise_chain(args, list, results) {

  return new Promise(function(done, errs) {
    var fn = list.shift();
    if (results === undefined) results = [];
    if (typeof fn === 'function') {
      fn(args).then(function(result) {
        results.push(result);
        console.log(result);
        promise_chain(result, list, results).then(done);
      }, errs);
    } else {
      done(results);
    }

  });

}

promise_chain(0, [tester1, tester2, tester1, tester2, tester2]).then(console.log.bind(console), console.error.bind(console));

cestmoi
źródło
0

Natknąłem się na tę stronę, próbując rozwiązać problem w NodeJS: ponowny montaż fragmentów plików. Zasadniczo: mam tablicę nazw plików. Muszę dołączyć wszystkie te pliki we właściwej kolejności, aby utworzyć jeden duży plik. Muszę to zrobić asynchronicznie.

Moduł 'fs' węzła zapewnia appendFileSync, ale nie chciałem blokować serwera podczas tej operacji. Chciałem użyć modułu fs.promises i znaleźć sposób na połączenie tych rzeczy razem. Przykłady na tej stronie nie do końca działały dla mnie, ponieważ faktycznie potrzebowałem dwóch operacji: fsPromises.read () do wczytania fragmentu pliku i fsPromises.appendFile () do połączenia z plikiem docelowym. Może gdybym był lepszy z javascriptem, mógłbym sprawić, że poprzednie odpowiedzi działałyby dla mnie. ;-)

Natknąłem się na to ... https://css-tricks.com/why-using-reduce-to-sequably-resolve-promises-works/ ... i udało mi się zhakować razem działające rozwiązanie.

TLDR:

/**
 * sequentially append a list of files into a specified destination file
 */
exports.append_files = function (destinationFile, arrayOfFilenames) {
    return arrayOfFilenames.reduce((previousPromise, currentFile) => {
        return previousPromise.then(() => {
            return fsPromises.readFile(currentFile).then(fileContents => {
                return fsPromises.appendFile(destinationFile, fileContents);
            });
        });
    }, Promise.resolve());
};

A oto test jednostkowy jaśminu:

const fsPromises = require('fs').promises;
const fsUtils = require( ... );
const TEMPDIR = 'temp';

describe("test append_files", function() {
    it('append_files should work', async function(done) {
        try {
            // setup: create some files
            await fsPromises.mkdir(TEMPDIR);
            await fsPromises.writeFile(path.join(TEMPDIR, '1'), 'one');
            await fsPromises.writeFile(path.join(TEMPDIR, '2'), 'two');
            await fsPromises.writeFile(path.join(TEMPDIR, '3'), 'three');
            await fsPromises.writeFile(path.join(TEMPDIR, '4'), 'four');
            await fsPromises.writeFile(path.join(TEMPDIR, '5'), 'five');

            const filenameArray = [];
            for (var i=1; i < 6; i++) {
                filenameArray.push(path.join(TEMPDIR, i.toString()));
            }

            const DESTFILE = path.join(TEMPDIR, 'final');
            await fsUtils.append_files(DESTFILE, filenameArray);

            // confirm "final" file exists    
            const fsStat = await fsPromises.stat(DESTFILE);
            expect(fsStat.isFile()).toBeTruthy();

            // confirm content of the "final" file
            const expectedContent = new Buffer('onetwothreefourfive', 'utf8');
            var fileContents = await fsPromises.readFile(DESTFILE);
            expect(fileContents).toEqual(expectedContent);

            done();
        }
        catch (err) {
            fail(err);
        }
        finally {
        }
    });
});

Mam nadzieję, że to komuś pomoże.

Sójka
źródło