Node.js natif Promise.all est-il traité en parallèle ou séquentiellement?


174

Je voudrais clarifier ce point, car la documentation n'est pas trop claire à ce sujet;

Q1: Le Promise.all(iterable)traitement de toutes les promesses est-il séquentiel ou parallèle? Ou, plus précisément, est-ce l'équivalent d'exécuter des promesses enchaînées comme

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

ou est - il un autre type d'algorithme où tout p1, p2, p3, p4, p5, etc. sont appelés en même temps (en parallèle) et les résultats sont renvoyés dès que tous resolve (ou on rejette)?

Q2: Si Promise.alls'exécute en parallèle, existe-t-il un moyen pratique d'exécuter un itérable séquentiellement?

Remarque : je ne veux pas utiliser Q ou Bluebird, mais toutes les spécifications ES6 natives.


Vous posez des questions sur la mise en œuvre du nœud (V8) ou sur les spécifications?
Amit le

1
Je suis à peu près sûr de les Promise.allexécuter en parallèle.
royhowie

@Amit j'ai signalé node.jset io.jscomme c'est là que je l'utilise. Donc, oui, l'implémentation V8 si vous voulez.
Yanick Rochon

9
Les promesses ne peuvent pas «être exécutées». Ils commencent leur tâche lors de leur création - ils ne représentent que les résultats - et vous exécutez tout en parallèle avant même de les transmettre Promise.all.
Bergi

Les promesses sont exécutées au moment de la création. (peut être confirmé en exécutant un peu de code). Dans new Promise(a).then(b); c();a est exécuté d'abord, puis c, puis b. Ce n'est pas Promise.all qui exécute ces promesses, il gère juste quand elles se résolvent.
Mateon1

Réponses:


258

Est-ce que l' Promise.all(iterable)exécution de toutes les promesses?

Non, les promesses ne peuvent pas «être exécutées». Ils commencent leur tâche lors de leur création - ils ne représentent que les résultats - et vous exécutez tout en parallèle avant même de les transmettre Promise.all.

Promise.allne fait que Attendre les multiples promesses. Peu importe dans quel ordre ils résolvent ou si les calculs sont exécutés en parallèle.

existe-t-il un moyen pratique d'exécuter un itératif de manière séquentielle?

Si vous avez déjà vos promesses, vous ne pouvez pas faire grand chose mais Promise.all([p1, p2, p3, …])(qui n'a pas de notion de séquence). Mais si vous avez un itérable de fonctions asynchrones, vous pouvez en effet les exécuter séquentiellement. Fondamentalement, vous devez obtenir de

[fn1, fn2, fn3, …]

à

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

et la solution pour ce faire est d'utiliser Array::reduce:

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

1
Dans cet exemple, un tableau des fonctions qui renvoient une promesse que vous souhaitez appeler est-il itérable?
James Reategui

2
@SSHThis: C'est exactement comme la thenséquence - la valeur de retour est la promesse du dernier fnrésultat, et vous pouvez enchaîner d'autres rappels à cela.
Bergi

1
@wojjas C'est exactement équivalent à fn1().then(p2).then(fn3).catch(…? Pas besoin d'utiliser une expression de fonction.
Bergi

1
@wojjas Bien sûr, le retValFromF1est transmis p2, c'est exactement ce que p2fait. Bien sûr, si vous voulez faire plus (passer des variables supplémentaires, appeler plusieurs fonctions, etc.), vous devez utiliser une expression de fonction, bien que changer p2dans le tableau serait plus facile
Bergi

1
@ robe007 Oui, je voulais dire que iterablec'est le [fn1, fn2, fn3, …]tableau
Bergi

62

En parallèle

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

Avantages: plus rapide. Toutes les itérations seront exécutées même en cas d'échec.

En séquence

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

Avantages: les variables de la boucle peuvent être partagées à chaque itération. Se comporte comme un code synchrone impératif normal.


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

1
@david_adler Dans les avantages des exemples parallèles, vous avez dit Toutes les itérations seront exécutées même si l'une échoue . Si je ne me trompe pas, cela échouerait toujours rapidement. Pour changer ce comportement, on peut faire quelque chose comme: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor

@Taimoor oui il "échoue rapidement" et continue d'exécuter le code après le Promise.all mais toutes les itérations sont toujours exécutées codepen.io/mfbx9da4/pen/BbaaXr
david_adler

Cette approche est meilleure, lorsque la asyncfonction est un appel d'API et que vous ne voulez pas DDOS le serveur. Vous avez un meilleur contrôle sur les résultats individuels et les erreurs générées lors de l'exécution. Mieux encore, vous pouvez décider des erreurs à continuer et de ce qu'il faut briser la boucle.
mandarin

Notez que javascript n'exécute pas réellement les requêtes asynchrones en "parallèle" en utilisant des threads car javascript est monothread. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler

11

La réponse de Bergis m'a mis sur la bonne voie en utilisant Array.reduce.

Cependant, pour que les fonctions renvoient mes promesses de s'exécuter l'une après l'autre, j'ai dû ajouter un peu plus d'imbrication.

Mon cas d'utilisation réel est un tableau de fichiers que je dois transférer les uns après les autres en raison des limites en aval ...

Voici ce avec quoi j'ai fini.

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

Comme le suggèrent les réponses précédentes, en utilisant:

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

n'a pas attendu la fin du transfert avant d'en démarrer un autre et le texte "Tous les fichiers transférés" est venu avant même le début du premier transfert de fichiers.

Je ne sais pas ce que j'ai fait de mal, mais je voulais partager ce qui a fonctionné pour moi.

Edit: Depuis que j'ai écrit ce post, je comprends maintenant pourquoi la première version n'a pas fonctionné. then () attend une fonction renvoyant une promesse. Donc, vous devez passer le nom de la fonction sans parenthèses! Maintenant, ma fonction veut un argument alors je dois encapsuler dans une fonction anonyme ne prenant aucun argument!


4

juste pour élaborer sur la réponse de @ Bergi (qui est très succincte, mais difficile à comprendre;)

Ce code exécutera chaque élément du tableau et ajoutera le prochain 'then chain' à la fin;

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

espérons que cela a du sens.


3

Vous pouvez également traiter un itératif de manière séquentielle avec une fonction asynchrone à l'aide d'une fonction récursive. Par exemple, étant donné un tableau aà traiter avec une fonction asynchrone 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"))


l'utilisation array.prototype.reduceest bien meilleure en termes de performances qu'une fonction récursive
Mateusz Sowiński

@ MateuszSowiński, il y a un délai de 1500 ms entre chaque appel. Étant donné que cela fait des appels asynchrones séquentiellement, il est difficile de voir à quel point cela est pertinent, même pour un retournement asynchrone très rapide.
Mark Meyer

Disons que vous devez exécuter 40 fonctions asynchrones très rapides les unes après les autres - l'utilisation de fonctions récursives obstruerait votre mémoire assez rapidement
Mateusz Sowiński

@ MateuszSowiński, que la pile ne se termine pas ici ... nous revenons après chaque appel. Comparez cela avec reduceoù vous devez construire la then()chaîne entière en une seule étape, puis l'exécuter.
Mark Meyer

Dans le 40e appel de la fonction séquentielle, le premier appel de la fonction est toujours en mémoire en attendant le retour de la chaîne de fonctions séquentielles
Mateusz Sowiński

3

NodeJS n'exécute pas les promesses en parallèle, il les exécute simultanément car il s'agit d'une architecture de boucle d'événements à thread unique. Il est possible d'exécuter les choses en parallèle en créant un nouveau processus enfant pour tirer parti du processeur multicœur.

Parallèle Vs Concurent

En fait, ce Promise.allque fait, c'est empiler les promesses dans la file d'attente appropriée (voir l'architecture de la boucle d'événements) les exécuter simultanément (appeler P1, P2, ...) puis attendre chaque résultat, puis résoudre le Promise.all avec toutes les promesses résultats. Promise.all échouera à la première promesse qui échouera, à moins que vous n'ayez géré vous-même le rejet.

Il y a une différence majeure entre parallèle et concurrent, le premier exécutera un calcul différent dans un processus séparé exactement au même moment et ils progresseront à ce rythme, tandis que l'autre exécutera les différents calculs l'un après l'autre sans attendre le précédent. calcul pour finir et progresser en même temps sans dépendre les uns des autres.

Enfin, pour répondre à votre question, Promise.allne s'exécutera ni en parallèle ni séquentiellement mais simultanément.


Ça n'est pas correct. NodeJS peut exécuter les choses en parallèle. NodeJS a un concept de thread de travail. Par défaut, le nombre de threads de travail est 4. Par exemple, si vous utilisez la bibliothèque de chiffrement pour hacher deux valeurs, vous pouvez les exécuter en parallèle. Deux threads de travail géreront la tâche. Bien sûr, votre CPU doit être multicœur pour prendre en charge le parallélisme.
Shihab

Ouais vous avez raison, c'est ce que j'ai dit à la fin du premier paragraphe, mais j'ai parlé du processus enfant, bien sûr, ils peuvent gérer des travailleurs.
Adrien De Peretti

2

En utilisant async await, un tableau de promesses peut facilement être exécuté séquentiellement:

let a = [promise1, promise2, promise3];

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

func();

Remarque: dans l'implémentation ci-dessus, si une promesse est rejetée, le reste ne sera pas exécuté.Si vous voulez que toutes vos promesses soient exécutées, enveloppez votre await a[i]();intérieurtry catch


2

parallèle

voir cet exemple

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

en exécutant le code, il console "APPELÉ" pour les six promesses et quand elles sont résolues, il console toutes les 6 réponses après expiration du délai en même temps


1

La réponse de Bergi m'a aidé à rendre l'appel synchrone.J'ai ajouté un exemple ci-dessous où nous appelons chaque fonction après l'appel de la fonction précédente.

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

Est-ce une réponse à la question initiale?
Giulio Caccin

0

Vous pouvez le faire par boucle for.

promesse de retour de fonction asynchrone

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

let clients = [client1, client2, client3];

si vous écrivez le code suivant, le client est créé en parallèle

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

alors tous les clients sont créés en parallèle. mais si vous souhaitez créer un client séquentiellement, vous devez utiliser la boucle for

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

puis tous les clients sont créés séquentiellement.

bon codage :)


8
Pour le moment, async/ awaitn'est disponible qu'avec un transpilateur ou en utilisant d' autres moteurs que Node. En outre, vous ne devriez vraiment pas mélanger asyncavec yield. Bien qu'ils agissent de la même manière avec un transpilateur et co, ils sont vraiment très différents et ne devraient normalement pas se substituer l'un à l'autre. Vous devez également mentionner ces restrictions car votre réponse est déroutante pour les programmeurs novices.
Yanick Rochon

0

J'utilise for of afin de résoudre des promesses séquentielles. Je ne sais pas si cela aide ici, mais c'est ce que j'ai fait.

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

run().then().catch()

0

cela pourrait répondre à une partie de votre question.

oui, vous pouvez enchaîner un tableau de fonctions de retour de promesse comme suit ... (cela passe le résultat de chaque fonction à la suivante). vous pouvez bien sûr le modifier pour passer le même argument (ou aucun argument) à chaque fonction.

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

Je suis tombé sur cette page en essayant de résoudre un problème dans NodeJS: réassemblage de morceaux de fichiers. Fondamentalement: j'ai un tableau de noms de fichiers. Je dois ajouter tous ces fichiers, dans le bon ordre, pour créer un seul gros fichier. Je dois le faire de manière asynchrone.

Le module 'fs' de Node fournit appendFileSync mais je ne voulais pas bloquer le serveur pendant cette opération. Je voulais utiliser le module fs.promises et trouver un moyen d'enchaîner ces choses. Les exemples de cette page ne fonctionnaient pas tout à fait pour moi car j'avais en fait besoin de deux opérations: fsPromises.read () pour lire le bloc de fichier et fsPromises.appendFile () pour concaténer le fichier de destination. Peut-être que si j'étais meilleur avec javascript, j'aurais pu faire fonctionner les réponses précédentes pour moi. ;-)

Je suis tombé sur ceci ... https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/ ... et j'ai pu pirater ensemble une solution de travail.

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

Et voici un test unitaire de jasmin pour cela:

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

J'espère que cela aide quelqu'un.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.