Node.js Promise.all è in elaborazione nativa in parallelo o in sequenza?


173

Vorrei chiarire questo punto, poiché la documentazione non è troppo chiara al riguardo;

Q1: è Promise.all(iterable)in corso l'elaborazione tutte le promesse in sequenza o in parallelo? O, più specificamente, è l'equivalente di eseguire promesse concatenate come

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

o è un altro tipo di algoritmo dove tutti p1, p2, p3, p4, p5, ecc vengono chiamati contemporaneamente (in parallelo) ei risultati vengono restituiti appena risolvono tutti (o uno scarti)?

Q2: se Promise.allviene eseguito in parallelo, esiste un modo conveniente per eseguire un iterabile in sequenza?

Nota : non voglio usare Q o Bluebird, ma tutte le specifiche ES6 native.


Stai chiedendo l'implementazione del nodo (V8) o le specifiche?
Tra il

1
Sono abbastanza sicuro Promise.allche li esegue in parallelo.
Royowie,

@Amit Ho segnalato node.jse io.jscome questo è dove lo sto usando. Quindi, sì, l'implementazione V8, se vuoi.
Yanick Rochon,

9
Le promesse non possono "essere eseguite". Iniziano il loro compito quando vengono creati - rappresentano solo i risultati - e si esegue tutto in parallelo anche prima di passarli Promise.all.
Bergi,

Le promesse vengono eseguite al momento della creazione. (può essere confermato eseguendo un po 'di codice). In new Promise(a).then(b); c();a viene eseguito prima, quindi c, quindi b. Non è Promise.all che esegue queste promesse, gestisce solo quando si risolvono.
Mateon1,

Risposte:


257

È Promise.all(iterable)l'esecuzione di tutte le promesse?

No, le promesse non possono "essere eseguite". Iniziano il loro compito quando vengono creati - rappresentano solo i risultati - e si esegue tutto in parallelo anche prima di passarli Promise.all.

Promise.allattende solo più promesse. Non importa in quale ordine risolvono o se i calcoli sono eseguiti in parallelo.

c'è un modo conveniente per eseguire un iterabile in sequenza?

Se hai già le tue promesse, non puoi fare molto ma Promise.all([p1, p2, p3, …])(che non ha una nozione di sequenza). Ma se hai un iterabile di funzioni asincrone, puoi effettivamente eseguirle in sequenza. Fondamentalmente devi andare da

[fn1, fn2, fn3, …]

per

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

e la soluzione per farlo sta usando Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())

1
In questo esempio, è iterabile una matrice di funzioni che restituiscono una promessa che si desidera chiamare?
James Reategui,

2
@SSHQuesto: è esattamente come la thensequenza: il valore restituito è la promessa dell'ultimo fnrisultato e puoi concatenare altri callback a quello.
Bergi,

1
@wojjas È esattamente equivalente a fn1().then(p2).then(fn3).catch(…? Non è necessario utilizzare un'espressione di funzione.
Bergi,

1
@wojjas Certo che retValFromF1è passato p2, è esattamente quello che p2fa. Certo, se vuoi fare di più (passa variabili aggiuntive, chiama più funzioni, ecc.) Devi usare un'espressione di funzione, sebbene cambiare p2l'array sarebbe più semplice
Bergi,

1
@ robe007 Sì, intendevo dire che iterableè l' [fn1, fn2, fn3, …]array
Bergi,

62

In parallelo

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

Vantaggi: più veloce. Tutte le iterazioni verranno eseguite anche se una fallisce.

In sequenza

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

Vantaggi: le variabili nel ciclo possono essere condivise da ogni iterazione. Si comporta come il normale codice sincrono imperativo.


7
Oppure:for (const item of items) await fetchItem(item);
Robert Penner,

1
@david_adler In esempi paralleli, hai detto che tutte le iterazioni verranno eseguite anche se una fallisce . Se non sbaglio questo fallirebbe comunque velocemente. Per cambiare questo comportamento si può fare qualcosa del tipo: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor,

@Taimoor sì, "fallisce velocemente" e continua l'esecuzione del codice dopo Promise.all ma tutte le iterazioni sono ancora eseguite codepen.io/mfbx9da4/pen/BbaaXr
david_adler

Questo approccio è migliore, quando la asyncfunzione è una chiamata API e non si desidera DDOS il server. Hai un migliore controllo sui singoli risultati e sugli errori generati nell'esecuzione. Ancora meglio puoi decidere su quali errori continuare e su cosa interrompere il ciclo.
mandarino,

Si noti che javascript non sta effettivamente eseguendo le richieste asincrone in "parallelo" utilizzando i thread poiché javascript è a thread singolo. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler

11

La risposta di Bergis mi ha portato sulla strada giusta usando Array.reduce.

Tuttavia, per ottenere effettivamente le funzioni che restituiscono le mie promesse di eseguirle una dopo l'altra, ho dovuto aggiungere altro annidamento.

Il mio caso d'uso reale è una matrice di file che devo trasferire in ordine uno dopo l'altro a causa dei limiti a valle ...

Ecco cosa ho finito.

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

Come suggeriscono le risposte precedenti, usando:

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

non ha aspettato il completamento del trasferimento prima di avviarne un altro e anche il testo "Tutti i file trasferiti" è arrivato prima che fosse avviato anche il primo trasferimento di file.

Non sono sicuro di cosa ho fatto di sbagliato, ma volevo condividere ciò che ha funzionato per me.

Modifica: da quando ho scritto questo post ora capisco perché la prima versione non ha funzionato. then () si aspetta che una funzione restituisca una promessa. Quindi, dovresti passare il nome della funzione senza parentesi! Ora, la mia funzione vuole un argomento, quindi ho bisogno di entrare in una funzione anonima senza prendere argomenti!


4

solo per approfondire la risposta di @ Bergi (che è molto succinta, ma difficile da capire;)

Questo codice eseguirà ogni elemento dell'array e aggiungerà il successivo 'quindi catena' alla fine;

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

spero che abbia un senso.


3

È inoltre possibile elaborare un iterabile in sequenza con una funzione asincrona utilizzando una funzione ricorsiva. Ad esempio, dato un array ada elaborare con la funzione asincrona 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"))


utilizza array.prototype.reduceè molto meglio in termini di prestazioni rispetto a una funzione ricorsiva
Mateusz Sowiński

@ MateuszSowiński, c'è un timeout di 1500ms tra ogni chiamata. Considerando che questo sta eseguendo chiamate asincrone in sequenza, è difficile capire quanto sia rilevante anche per una rapida inversione di tendenza asincrona.
Mark Meyer,

Diciamo che devi eseguire 40 di funzioni asincrone molto veloci una dopo l'altra - l'uso di funzioni ricorsive ostruirà la tua memoria abbastanza velocemente
Mateusz Sowiński,

@ MateuszSowiński, che lo stack non finisce qui ... stiamo tornando dopo ogni chiamata. Confrontalo con il punto in reducecui devi costruire l'intera then()catena in un solo passaggio ed eseguire.
Mark Meyer,

Nella 40a chiamata della funzione sequenziale la prima chiamata della funzione è ancora in memoria in attesa del ritorno della catena di funzioni sequenziali
Mateusz Sowiński,

3

NodeJS non esegue le promesse in parallelo, le esegue contemporaneamente poiché si tratta di un'unica architettura a thread di eventi. Esiste la possibilità di eseguire le cose in parallelo creando un nuovo processo figlio per sfruttare la CPU multi-core.

Parallel Vs Concurent

In effetti, ciò che Promise.allfa è, impilando la funzione promesse nella coda appropriata (vedi architettura del ciclo di eventi) eseguendoli contemporaneamente (chiama P1, P2, ...) quindi aspettando ogni risultato, quindi risolvendo Promise.all con tutte le promesse risultati. Promise.all fallirà alla prima promessa che fallisce, a meno che tu non abbia gestito tu stesso il rifiuto.

Esiste una differenza sostanziale tra parallelo e simultaneo, il primo eseguirà un calcolo diverso in un processo separato esattamente nello stesso momento e progrediranno a quel ritmo, mentre l'altro eseguirà il calcolo diverso uno dopo l'altro senza attendere il precedente calcolo per finire e progredire allo stesso tempo senza dipendere l'uno dall'altro.

Infine, per rispondere alla tua domanda, Promise.allnon verrà eseguito né in parallelo né in sequenza ma contemporaneamente.


Questo non è giusto. NodeJS può eseguire cose in parallelo. NodeJS ha un concetto di thread di lavoro. Per impostazione predefinita, il numero di thread di lavoro è 4. Ad esempio, se si utilizza la libreria crypto per eseguire l'hashing di due valori, è possibile eseguirli in parallelo. Due thread di lavoro gestiranno l'attività. Ovviamente, la tua CPU deve essere multi-core per supportare il parallelismo.
Shihab,

Sì, hai ragione, è quello che ho detto alla fine del primo paragrafo, ma ho parlato del processo figlio, ovviamente possono gestire i lavoratori.
Adrien De Peretti,

2

L'uso di async wait attende una serie di promesse che possono essere facilmente eseguite in sequenza:

let a = [promise1, promise2, promise3];

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

func();

Nota: Nell'implementazione di cui sopra, se una promessa viene rifiutata, il resto non verrà eseguito. Se desideri che tutte le promesse vengano eseguite, avvolgi il tuo await a[i]();internotry catch


2

parallelo

vedi questo esempio

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

eseguendo il codice consolerà "CHIAMATO" per tutte e sei le promesse e una volta risolte consolerà ogni 6 risposte dopo il timeout contemporaneamente


1

La risposta di Bergi mi ha aiutato a rendere la chiamata sincrona. Ho aggiunto un esempio di seguito in cui chiamiamo ogni funzione dopo che è stata chiamata la funzione precedente.

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

È una risposta alla domanda originale?
Giulio Caccin,

0

Puoi farlo per ciclo.

promessa di ritorno della funzione asincrona

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

let clients = [client1, client2, client3];

se si scrive il seguente codice, il client viene creato parallelamente

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

quindi tutti i client vengono creati parallelamente. ma se si desidera creare un client in sequenza, è necessario utilizzare per il ciclo

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

quindi tutti i client vengono creati in sequenza.

buona programmazione :)


8
Al momento, async/ awaitè disponibile solo con un transpiler o utilizza motori diversi da Node. Inoltre, davvero non dovrebbe mescolare asynccon yield. Whle si comportano allo stesso modo con un transpiler e co, in realtà, sono abbastanza diversi e non dovrebbero sostituirsi a vicenda. Inoltre, dovresti menzionare queste restrizioni poiché la tua risposta è confusa per i programmatori alle prime armi.
Yanick Rochon,

0

Sto usando per of per risolvere le promesse sequenziali. Non sono sicuro se aiuta qui, ma questo è quello che ho fatto.

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

run().then().catch()

0

questo potrebbe rispondere a una parte della tua domanda.

sì, puoi concatenare una serie di funzioni di ritorno promesse come segue ... (questo passa il risultato di ciascuna funzione alla successiva). puoi ovviamente modificarlo per passare lo stesso argomento (o nessun argomento) a ciascuna funzione.

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


0

Mi sono imbattuto in questa pagina mentre cercavo di risolvere un problema in NodeJS: riassemblaggio di blocchi di file. Fondamentalmente: ho una serie di nomi di file. Devo aggiungere tutti quei file, nell'ordine corretto, per creare un file di grandi dimensioni. Devo farlo in modo asincrono.

Il modulo 'fs' del nodo fornisce appendFileSync ma non volevo bloccare il server durante questa operazione. Volevo usare il modulo fs.promises e trovare un modo per concatenare queste cose. Gli esempi in questa pagina non hanno funzionato abbastanza per me perché in realtà avevo bisogno di due operazioni: fsPromises.read () per leggere nel blocco di file e fsPromises.appendFile () per concatenare il file di destinazione. Forse se fossi stato meglio con JavaScript avrei potuto far funzionare le risposte precedenti per me. ;-)

Mi sono imbattuto in questo ... https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/ ... e sono stato in grado di hackerare insieme una soluzione funzionante.

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

Ed ecco un test unitario al gelsomino per questo:

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

Spero che aiuti qualcuno.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.