Wird Node.js native Promise.all parallel oder nacheinander verarbeitet?

173

Ich möchte diesen Punkt klarstellen, da die Dokumentation darüber nicht zu klar ist.

Q1: Ist die Promise.all(iterable)Verarbeitung aller Versprechungen der Reihe nach oder parallel? Oder genauer gesagt, ist es das Äquivalent zu verketteten Versprechungen wie

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

oder ist es eine andere Art von Algorithmus , bei dem alle p1, p2, p3, p4, p5etc. zur gleichen Zeit (parallel) genannt werden und die Ergebnisse werden, sobald alle resolve (oder ein Spuck) zurück?

F2: Wenn Promise.allparallel ausgeführt wird, gibt es eine bequeme Möglichkeit, eine iterierbare Sequenz sequentiell auszuführen?

Hinweis : Ich möchte nicht Q oder Bluebird verwenden, sondern alle nativen ES6-Spezifikationen.

Yanick Rochon
quelle
Fragen Sie nach der Implementierung von Node (V8) oder nach der Spezifikation?
Amit
1
Ich bin mir ziemlich sicher, dass Promise.allsie parallel ausgeführt werden.
Royhowie
@Amit Ich habe markiert node.jsund io.jsda ich es hier benutze. Also ja, die V8-Implementierung, wenn Sie so wollen.
Yanick Rochon
9
Versprechen können nicht "ausgeführt" werden. Sie beginnen ihre Aufgabe, wenn sie erstellt werden - sie stellen nur die Ergebnisse dar - und Sie führen alles parallel aus, noch bevor Sie sie an übergeben Promise.all.
Bergi
Versprechen werden im Moment der Schöpfung ausgeführt. (Kann durch Ausführen von Code bestätigt werden). In new Promise(a).then(b); c();a wird zuerst ausgeführt, dann c, dann b. Es ist nicht Promise.all, das diese Versprechen ausführt, sondern nur, wenn sie gelöst werden.
Mateon1

Antworten:

256

Ist die Promise.all(iterable)Ausführung aller Versprechungen?

Nein, Versprechen können nicht "ausgeführt" werden. Sie beginnen ihre Aufgabe, wenn sie erstellt werden - sie stellen nur die Ergebnisse dar - und Sie führen alles parallel aus, noch bevor Sie sie an übergeben Promise.all.

Promise.allwartet nur auf mehrere Versprechen. Es ist egal, in welcher Reihenfolge sie aufgelöst werden oder ob die Berechnungen parallel ausgeführt werden.

Gibt es eine bequeme Möglichkeit, eine iterierbare Sequenz sequentiell auszuführen?

Wenn Sie bereits Ihre Versprechen haben, können Sie nicht viel tun, aber Promise.all([p1, p2, p3, …])(was keine Vorstellung von Sequenz hat). Wenn Sie jedoch über eine Iteration asynchroner Funktionen verfügen, können Sie diese tatsächlich nacheinander ausführen. Grundsätzlich müssen Sie von bekommen

[fn1, fn2, fn3, …]

zu

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

und die Lösung dafür ist Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())
Bergi
quelle
1
Ist in diesem Beispiel ein Array der Funktionen iterierbar, die ein Versprechen zurückgeben, das Sie aufrufen möchten?
James Reategui
2
@SSHThis: Es ist genau wie die thenSequenz - der Rückgabewert ist das Versprechen für das letzte fnErgebnis, und Sie können andere Rückrufe daran ketten.
Bergi
1
@wojjas Das ist genau gleichbedeutend mit fn1().then(p2).then(fn3).catch(…? Es muss kein Funktionsausdruck verwendet werden.
Bergi
1
@wojjas Natürlich retValFromF1wird das weitergegeben p2, genau das p2macht es. Sicher, wenn Sie mehr tun möchten (zusätzliche Variablen übergeben, mehrere Funktionen aufrufen usw.), müssen Sie einen Funktionsausdruck verwenden, obwohl das Ändern p2im Array einfacher wäre
Bergi
1
@ robe007 Ja, ich meinte, das iterableist das [fn1, fn2, fn3, …]Array
Bergi
62

Parallel zu

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

Vorteile: Schneller. Alle Iterationen werden ausgeführt, auch wenn eine fehlschlägt.

Der Reihe nach

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

Vorteile: Variablen in der Schleife können von jeder Iteration gemeinsam genutzt werden. Benimmt sich wie normaler imperativer synchroner Code.

david_adler
quelle
7
Oder:for (const item of items) await fetchItem(item);
Robert Penner
1
@david_adler Parallel dazu haben Sie gesagt, dass alle Iterationen ausgeführt werden, auch wenn eine fehlschlägt . Wenn ich mich nicht irre, würde dies immer noch schnell scheitern. Um dieses Verhalten zu ändern, kann man etwas tun wie: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor
@Taimoor ja, es schlägt "schnell fehl" und führt die Ausführung von Code nach Promise.all weiter aus, aber alle Iterationen werden weiterhin ausgeführt. Codepen.io/mfbx9da4/pen/BbaaXr
david_adler
Dieser Ansatz ist besser, wenn die asyncFunktion ein API-Aufruf ist und Sie den Server nicht als DDOS verwenden möchten. Sie haben eine bessere Kontrolle über die einzelnen Ergebnisse und Fehler, die bei der Ausführung auftreten. Noch besser können Sie entscheiden, welche Fehler fortgesetzt und welche Schleife unterbrochen werden soll.
Mandarin
Beachten Sie, dass Javascript die asynchronen Anforderungen nicht "parallel" mithilfe von Threads ausführt, da Javascript ein Single-Thread ist. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler
11

Die Antwort von Bergis hat mich mit Array.reduce auf den richtigen Weg gebracht.

Um jedoch tatsächlich zu erreichen, dass die Funktionen meine Versprechen zurückgeben, nacheinander ausgeführt zu werden, musste ich weitere Verschachtelungen hinzufügen.

Mein eigentlicher Anwendungsfall ist eine Reihe von Dateien, die ich aufgrund von nachgeschalteten Einschränkungen nacheinander übertragen muss ...

Hier ist, was ich am Ende hatte.

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

Wie aus den vorherigen Antworten hervorgeht, verwenden Sie:

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

Ich habe nicht auf den Abschluss der Übertragung gewartet, bevor ich eine weitere gestartet habe, und auch der Text "Alle übertragenen Dateien" wurde angezeigt, bevor die erste Dateiübertragung gestartet wurde.

Ich bin mir nicht sicher, was ich falsch gemacht habe, wollte aber mitteilen, was für mich funktioniert hat.

Bearbeiten: Seit ich diesen Beitrag geschrieben habe, verstehe ich jetzt, warum die erste Version nicht funktioniert hat. then () erwartet eine Funktion, die ein Versprechen zurückgibt. Sie sollten also den Funktionsnamen ohne Klammern übergeben! Jetzt möchte meine Funktion ein Argument, also muss ich mich in eine anonyme Funktion einfügen, die kein Argument akzeptiert!

tkarls
quelle
4

nur um auf @ Bergis Antwort einzugehen (was sehr prägnant, aber schwierig zu verstehen ist;)

Dieser Code führt jedes Element im Array aus und fügt am Ende die nächste 'then-Kette' hinzu.

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

hoffe das macht Sinn.

TimoSolo
quelle
3

Sie können eine iterierbare Funktion auch sequentiell mit einer asynchronen Funktion mithilfe einer rekursiven Funktion verarbeiten. Beispiel: Ein Array a, das mit asynchroner Funktion verarbeitet werden soll 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
quelle
Verwenden array.prototype.reduceist in Bezug auf die Leistung viel besser als eine rekursive Funktion
Mateusz Sowiński
@ MateuszSowiński, zwischen jedem Anruf liegt eine Zeitüberschreitung von 1500 ms. Wenn man bedenkt, dass dies asynchrone Aufrufe nacheinander ausführt, ist es schwer zu erkennen, wie relevant dies selbst für einen sehr schnellen asynchronen Turnaround ist.
Mark Meyer
Angenommen, Sie müssen 40 wirklich schnelle asynchrone Funktionen nacheinander ausführen - die Verwendung rekursiver Funktionen würde Ihr Gedächtnis ziemlich schnell verstopfen
Mateusz Sowiński
@ MateuszSowiński, dass der Stapel hier nicht landet ... wir kehren nach jedem Aufruf zurück. Vergleichen Sie das damit, reducewo Sie die gesamte then()Kette in einem Schritt erstellen und dann ausführen müssen.
Mark Meyer
Beim 40. Aufruf der sequentiellen Funktion befindet sich der erste Aufruf der Funktion noch im Speicher und wartet darauf, dass die Kette der sequentiellen Funktionen zurückkehrt
Mateusz Sowiński
2

Mit async wait kann eine Reihe von Versprechungen einfach nacheinander ausgeführt werden:

let a = [promise1, promise2, promise3];

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

func();

Hinweis: Wenn in der obigen Implementierung ein Versprechen abgelehnt wird, wird der Rest nicht ausgeführt. Wenn Sie möchten, dass alle Ihre Versprechen ausgeführt werden, wickeln Sie Ihr await a[i]();Inneres eintry catch

Ayan
quelle
2

parallel

siehe dieses Beispiel

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

Durch Ausführen des Codes wird "CALLED" für alle sechs Versprechen getröstet, und wenn sie gelöst sind, werden alle 6 Antworten nach einer Zeitüberschreitung gleichzeitig getröstet

Chintan Rajpara
quelle
2

NodeJS führt Versprechen nicht parallel aus, sondern gleichzeitig, da es sich um eine Single-Threaded-Event-Loop-Architektur handelt. Es besteht die Möglichkeit, Dinge parallel auszuführen, indem ein neuer untergeordneter Prozess erstellt wird, um die Vorteile der Mehrkern-CPU zu nutzen.

Parallel Vs Concurent

In der Tat, was Promise.all stapeln Sie die Versprechensfunktion in der entsprechenden Warteschlange (siehe Ereignisschleifenarchitektur) sie gleichzeitig ausführen (Aufruf P1, P2, ...), dann auf jedes Ergebnis warten und dann Promise.all mit allen Versprechungen auflösen Ergebnisse. Promise.all schlägt beim ersten Versprechen fehl, das fehlschlägt, es sei denn, Sie haben die Ablehnung selbst verwaltet.

Es gibt einen großen Unterschied zwischen parallel und gleichzeitig, der erste führt unterschiedliche Berechnungen in einem separaten Prozess genau zur gleichen Zeit aus und sie werden in diesem Rhythmus fortschreiten, während der andere die unterschiedlichen Berechnungen nacheinander ausführt, ohne auf den vorherigen zu warten Berechnung zu beenden und gleichzeitig fortzufahren, ohne voneinander abhängig zu sein.

Um Ihre Frage zu beantworten, Promise.allwird sie weder parallel noch sequentiell, sondern gleichzeitig ausgeführt.

Adrien De Peretti
quelle
Das ist nicht richtig. NodeJS kann Dinge parallel ausführen. NodeJS hat ein Konzept des Arbeitsthreads. Standardmäßig beträgt die Anzahl der Arbeitsthreads 4. Wenn Sie beispielsweise die Kryptobibliothek verwenden, um zwei Werte zu hashen, können Sie sie parallel ausführen. Zwei Arbeitsthreads übernehmen die Aufgabe. Natürlich muss Ihre CPU mehrkernig sein, um Parallelität zu unterstützen.
Shihab
Ja, Sie haben Recht, das habe ich am Ende des ersten Absatzes gesagt, aber ich habe über den Kinderprozess gesprochen, natürlich können sie Arbeiter führen.
Adrien De Peretti
1

Bergis Antwort hat mir geholfen, den Aufruf synchron zu machen. Ich habe unten ein Beispiel hinzugefügt, in dem wir jede Funktion aufrufen, nachdem die vorherige Funktion aufgerufen wurde.

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
quelle
Ist das eine Antwort auf die ursprüngliche Frage?
Giulio Caccin
0

Sie können es mit for-Schleife tun.

Rückgabeversprechen der asynchronen Funktion

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

let clients = [client1, client2, client3];

Wenn Sie folgenden Code schreiben, werden die Clients parallel erstellt

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

dann werden alle Clients parallel erstellt. Wenn Sie den Client jedoch nacheinander erstellen möchten, sollten Sie die for-Schleife verwenden

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

dann werden alle Clients nacheinander erstellt.

Happy Coding :)

Deepak Sisodiya
quelle
8
Derzeit ist async/ awaitnur mit einem Transpiler oder mit anderen Motoren als Node verfügbar . Außerdem sollten Sie wirklich nicht mischen asyncmit yield. Während sie sich mit einem Transpiler gleich verhalten und cowirklich sehr unterschiedlich sind und sich normalerweise nicht gegenseitig beeinflussen sollten. Sie sollten diese Einschränkungen auch erwähnen, da Ihre Antwort für unerfahrene Programmierer verwirrend ist.
Yanick Rochon
0

Ich habe für verwendet, um sequentielle Versprechen zu lösen. Ich bin mir nicht sicher, ob es hier hilft, aber das habe ich getan.

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

run().then().catch()
Nick Kotenberg
quelle
0

Dies könnte einen Teil Ihrer Frage beantworten.

Ja, Sie können ein Array von Versprechen zurückgebender Funktionen wie folgt verketten ... (dies übergibt das Ergebnis jeder Funktion an die nächste). Sie können es natürlich bearbeiten, um jeder Funktion dasselbe Argument (oder keine Argumente) zu übergeben.

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

das ist Französisch
quelle
0

Ich bin über diese Seite gestolpert, als ich versucht habe, ein Problem in NodeJS zu lösen: das Zusammensetzen von Dateiblöcken. Grundsätzlich gilt: Ich habe eine Reihe von Dateinamen. Ich muss alle diese Dateien in der richtigen Reihenfolge anhängen, um eine große Datei zu erstellen. Ich muss das asynchron machen.

Das 'fs'-Modul des Knotens bietet zwar appendFileSync, aber ich wollte den Server während dieses Vorgangs nicht blockieren. Ich wollte das Modul fs.promises verwenden und einen Weg finden, dieses Zeug miteinander zu verketten. Die Beispiele auf dieser Seite haben für mich nicht ganz funktioniert, da ich tatsächlich zwei Operationen benötigte: fsPromises.read () zum Einlesen des Dateiblocks und fsPromises.appendFile () zum Konzentrieren auf die Zieldatei. Wenn ich besser mit Javascript umgehen könnte, hätte ich vielleicht die vorherigen Antworten für mich arbeiten lassen können. ;-);

Ich bin darauf gestoßen ... https://css-tricks.com/why-using-reduce-to-sequential-resolve-promises-works/ ... und konnte eine funktionierende Lösung zusammen hacken.

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

Und hier ist ein Jasmin-Unit-Test dafür:

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

Ich hoffe es hilft jemandem.

Jay
quelle