Appeler les fonctions asynchrones / attente en parallèle

434

Pour autant que je sache, dans ES7 / ES2016, mettre plusieurs awaitdans le code fonctionnera de manière similaire au chaînage .then()avec des promesses, ce qui signifie qu'ils s'exécuteront les uns après les autres plutôt qu'en parallèle. Ainsi, par exemple, nous avons ce code:

await someCall();
await anotherCall();

Dois-je comprendre correctement que anotherCall()sera appelé uniquement une fois someCall()terminé? Quelle est la manière la plus élégante de les appeler en parallèle?

Je veux l'utiliser dans Node, alors peut-être qu'il y a une solution avec la bibliothèque asynchrone?

EDIT: Je ne suis pas satisfait de la solution fournie dans cette question: Ralentissement dû à l'attente non parallèle de promesses dans les générateurs asynchrones , car il utilise des générateurs et je pose des questions sur un cas d'utilisation plus général.

Victor Marchuk
la source
1
@adeneo C'est incorrect, Javascript ne s'exécute jamais en parallèle dans son propre contexte.
Blindman67
5
@ Blindman67 - il le fait, au moins de la même façon que l'OP, où deux opérations asynchrones s'exécutent simultanément, mais pas dans ce cas, ce que je voulais écrire était qu'elles s'exécutaient en série , la première awaitattendrait que la première fonction se termine entièrement avant d'exécuter la seconde.
adeneo
3
@ Blindman67 - c'est un thread unique, mais cette limitation ne s'applique pas aux méthodes asynchrones, elles peuvent s'exécuter simultanément et renvoyer la réponse lorsqu'elles sont terminées, c'est-à-dire ce que l'OP signifie par "parallell".
adeneo
7
@ Blindman67 - Je pense qu'il est assez clair ce que l'OP demande, l'utilisation du modèle async / attente fera fonctionner les fonctions en série, même si elles sont asynchrones, donc la première se terminerait complètement avant l'appel de la seconde, etc. L'OP est demandant comment appeler les deux fonctions en parallèle, et comme elles sont clairement asynchrones, le but est de les exécuter simultanément, c'est-à-dire en parallèle, par exemple en faisant deux requêtes ajax simultanément, ce qui n'est pas du tout un problème en javascript, comme la plupart des méthodes asynchrones , comme vous l'avez noté, exécute du code natif et utilise plus de threads.
adeneo
3
@Bergi ce n'est pas un doublon de la question liée - il s'agit spécifiquement de la syntaxe asynchrone / wait et des Promises natifs . La question liée concerne la bibliothèque Bluebird avec générateurs et rendement. Conceptuellement similaire peut-être, mais pas dans la mise en œuvre.
Iest

Réponses:

703

Vous pouvez attendre sur Promise.all():

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

Pour stocker les résultats:

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

Notez que cela Promise.alléchoue rapidement, ce qui signifie que dès qu'une des promesses qui lui sont faites rejette, alors la chose entière rejette.

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'

Si, à la place, vous voulez attendre que toutes les promesses soient tenues ou rejetées, vous pouvez utiliser Promise.allSettled. Notez qu'Internet Explorer ne prend pas en charge nativement cette méthode.

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
la source
79
Nettoyez mais soyez conscient du comportement de défaillance rapide de Promise.all. Si l'une des fonctions génère une erreur, Promise.all rejettera
NoNameProvided
11
Vous pouvez bien gérer les résultats partiels avec async /
wait
131
Conseil de pro: utilisez la déstructuration des tableaux afin d'initialiser un nombre arbitraire de résultats à partir de Promise.all (), comme:[result1, result2] = Promise.all([async1(), async2()]);
jonny
10
@jonny Est-ce sujet à l'échec rapide? En a-t-on encore besoin = await Promise.all?
theUtherSide
5
@theUtherSide Vous avez absolument raison - j'ai négligé d'inclure l'attente.
jonny
114

TL; DR

Utilisez Promise.allpour les appels de fonctions parallèles, les comportements de réponse ne sont pas correctement lorsque l'erreur se produit.


Tout d'abord, exécutez tous les appels asynchrones à la fois et obtenez tous les Promiseobjets. Deuxièmement, utilisez awaitsur les Promiseobjets. De cette façon, pendant que vous attendez que le premier Promiserésolve les autres appels asynchrones progressent toujours. Dans l'ensemble, vous n'attendez que le temps de l'appel asynchrone le plus lent. Par exemple:

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

Exemple JSbin: http://jsbin.com/xerifanima/edit?js,console

Mise en garde: Peu importe si les awaitappels sont sur la même ligne ou sur des lignes différentes, tant que le premier awaitappel se produit après tous les appels asynchrones. Voir le commentaire de JohnnyHK.


Mise à jour: cette réponse a un timing différent dans la gestion des erreurs selon la réponse de @ bergi , elle ne jette PAS l'erreur car l'erreur se produit mais après que toutes les promesses ont été exécutées. Je compare le résultat avec le conseil de @ jonny:, [result1, result2] = Promise.all([async1(), async2()])vérifiez l'extrait de code suivant

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

Havre
la source
11
Cela ressemble à une option beaucoup plus agréable que Promise.all - et avec une affectation de déstructuration, vous pouvez même le faire [someResult, anotherResult] = [await someResult, await anotherResult]si vous passez constà let.
jawj
28
Mais cela exécute toujours les awaitdéclarations en série, non? Autrement dit, l'exécution s'interrompt jusqu'à ce que le premier soit awaitrésolu, puis passe au second. Promise.alls'exécute en parallèle.
Andru
8
Merci @Haven. Ce devrait être la réponse acceptée.
Stefan D
87
Cette réponse est trompeuse car le fait que les deux attentes se fassent sur la même ligne n'est pas pertinent. Ce qui importe, c'est que les deux appels asynchrones soient effectués avant que l'un ou l'autre ne soit attendu.
JohnnyHK
15
@Haven cette solution n'est pas la même que Promise.all. Si chaque demande est un appel réseau, await someResultdevra être résolue avant await anotherResultmême de commencer. Inversement, Promise.allles deux awaitappels peuvent être démarrés avant que l'un d'eux ne soit résolu.
Ben Winding
89

Mise à jour:

La réponse originale rend difficile (et dans certains cas impossible) de gérer correctement les refus de promesse. La bonne solution consiste à utiliser Promise.all:

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

Réponse originale:

Assurez-vous simplement d'appeler les deux fonctions avant d'attendre l'une ou l'autre:

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

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
la source
1
@JeffFischer J'ai ajouté des commentaires qui, je l'espère, le rendront plus clair.
Jonathan Potter
9
J'ai l'impression que c'est certainement la réponse la plus pure
Gershom
1
Cette réponse est beaucoup plus claire que celle de Haven. Il est clair que les appels de fonction renverront des objets promis, awaitpuis les résoudront en valeurs réelles.
user1032613
3
Cela semble fonctionner d'un coup d'œil, mais a des problèmes horribles avec les rejets non gérés . Ne l'utilisez pas!
Bergi
1
@Bergi Vous avez raison, merci de l'avoir signalé! J'ai mis à jour la réponse avec une meilleure solution.
Jonathan Potter
24

Il existe une autre façon sans Promise.all () de le faire en parallèle:

Tout d'abord, nous avons 2 fonctions pour imprimer les nombres:

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

C'est séquentiel:

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

C'est parallèle:

async function inParallel() {
   const promise1 = printNumber1();
   const promise2 = printNumber2();
   const number1 = await promise1;
   const number2 = await promise2;
}
//Output: Number2 is done, Number1 is done
user2883596
la source
10

Cela peut être accompli avec Promise.allSettled () , qui est similaire Promise.all()mais sans le comportement de sécurité rapide.

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

Remarque : Il s'agit d'une fonctionnalité de pointe avec une prise en charge limitée du navigateur, je recommande donc fortement d' inclure un polyfill pour cette fonction.

Jonathan Sudiaman
la source
7

J'ai créé un résumé testant différentes façons de résoudre les promesses, avec des résultats. Il peut être utile de voir les options qui fonctionnent.

SkarXa
la source
Les tests 4 et 6 de l'essentiel ont renvoyé les résultats attendus. Voir stackoverflow.com/a/42158854/5683904 par NoNameProvided qui explique la différence entre les options.
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
    */

Bien que définir p1, p2 et p3 ne les exécute pas strictement en parallèle, ils ne bloquent aucune exécution et vous pouvez intercepter les erreurs contextuelles avec un catch.

Thrunobulax
la source
2
Bienvenue dans Stack Overflow. Bien que votre code puisse fournir la réponse à la question, veuillez ajouter du contexte pour que les autres aient une idée de ce qu'il fait et pourquoi il est là.
Theo
1

Dans mon cas, j'ai plusieurs tâches que je veux exécuter en parallèle, mais je dois faire quelque chose de différent avec le résultat de ces tâches.

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

Et la sortie:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
la source
cool pour la création dynamique (éventail de ressources)
Michal Miky Jankovský
1

attendre Promise.all ([someCall (), anotherCall ()]); comme déjà mentionné agira comme une barrière de thread (très courant dans le code parallèle comme CUDA), donc il permettra à toutes les promesses qu'il contient de s'exécuter sans se bloquer, mais empêchera l'exécution de continuer jusqu'à ce que TOUS soient résolus.

une autre approche qui mérite d'être partagée est l'async Node.js qui vous permettra également de contrôler facilement la quantité de simultanéité qui est généralement souhaitable si la tâche est directement liée à l'utilisation de ressources limitées comme appel API, opérations d'E / S, etc.

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

Crédits à l'auteur de l'article Medium (en savoir plus )

Thiago Conrado
la source
-5

Je vote pour:

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

Soyez conscient du moment où vous appelez des fonctions, cela peut entraîner un résultat inattendu:

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

Mais le suivi déclenche toujours une demande de création d'un nouvel utilisateur

// 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
la source
Puisque vous déclarez la fonction en dehors / avant le test de condition, et les appelez. Essayez de les emballer en elsebloc.
Haven
@Haven: Je veux dire que lorsque vous séparez les moments où vous appelez des fonctions vs attendre, cela peut conduire à des résultats inattendus, par exemple: des requêtes HTTP asynchrones.
Hoang Le Anh Tu
-6

Je crée une fonction d'aide waitAll, peut-être que cela peut la rendre plus douce. Cela ne fonctionne que dans nodejs pour l'instant, pas dans le chrome du navigateur.

    //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
la source
3
Non, la parallélisation ne se produit pas du tout ici. La forboucle attend séquentiellement chaque promesse et ajoute le résultat au tableau.
Szczepan Hołyszewski
Je comprends que cela ne fonctionne pas pour les gens. J'ai donc testé dans node.js et navigateur. Le test est réussi dans node.js (v10, v11), firefox, il ne fonctionne pas dans le navigateur chrome. Le cas de test se trouve dans gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang
2
Je refuse de le croire. Rien dans la norme ne dit que différentes itérations d'une boucle for peuvent être parallélisées automatiquement; ce n'est pas ainsi que fonctionne javascript. La façon dont le code de la boucle est écrit, cela signifie ceci: "attendre un élément (l'attente expr), ALORS pousser le résultat vers temp, ALORS prendre l'élément suivant (prochaine itération de la boucle for)." L'attente "pour chaque élément est complètement confiné à une seule itération de la boucle. Si les tests montrent qu'il y a parallélisation, cela doit être parce que le transpilateur fait quelque chose de non standard ou est
bogué à
@ SzczepanHołyszewski Votre confiance de faire preuve de discernement sans exécuter le scénario de test m'inspire à faire des changements de noms et des commentaires supplémentaires. Tous les codes sont de l'ancien ES6, aucun transpiling n'est requis.
Fred Yang
Je ne sais pas pourquoi cela est si fortement rejeté. C'est essentiellement la même réponse que @ user2883596 a donnée.
Jonathan Sudiaman