web-dev-qa-db-fra.com

Comment exécuter async/wait en parallèle en Javascript

Enfin, async/await sera supporté dans tous les principaux navigateurs bientôt, sauf IE . Nous pouvons donc commencer à écrire du code plus lisible avec async/await mais il y a un problème. Beaucoup de gens utilisent async comme cela:

const userResponse = await fetchUserAsync();
const postsResponse = await fetchPostsAsync();

Bien que ce code soit lisible, il pose un problème, il exécute les fonctions en série. Il ne commence pas à récupérer les publications tant que l'utilisateur n'a pas récupéré. La solution est simple, nous devons récupérer les ressources en parallèle.

Donc ce que je veux faire est (en pseudo langage): 

fn task() {
  result-1 = doAsync();
  result-2 = doAsync();
  result-n = doLongAsync();

  // handle results together
  combinedResult = handleResults(result-1, result-2);

  lastResult = handleLastResult(result-n);
}
43
NoNameProvided

Vous pouvez écrire quelque chose comme ceci:

const responses = await Promise.all([
 fetchUserAsync(),
 fetchPostsAsync(),
]);

const userResponse = responses[0];
const postsResponse = responses[1];

C'est facile non? Mais il ya un hic. Promise.all a le comportement fail-fast, ce qui signifie qu'il rejettera dès qu'une des promesses sera rejetée. Vous voulez probablement une solution plus robuste dans laquelle nous sommes en charge de gérer les rejets, aucun des extractions. Heureusement, il existe une solution, elle peut être réalisée simplement avec async/await sans avoir besoin d'utiliser Promise.all. Un exemple de travail: 

console.clear();

function wait(ms, data) {
  return new Promise( resolve => setTimeout(resolve.bind(this, data), ms) );
}

/** 
 * This will run in series, because 
 * we call a function and immediately wait for it's result, 
 * so this will finish in 1s.
 */
async function series() {
  return {
    result1: await wait(500, 'seriesTask1'),
    result2: await wait(500, 'seriesTask2'),
  }
}

/** 
 * While here we call the functions first,
 * then wait for the result later, so 
 * this will finish in 500ms.
 */
async function parallel() {
  const task1 = wait(500, 'parallelTask1');
  const task2 = wait(500, 'parallelTask2');

  return {
    result1: await task1,
    result2: await task2,
  }
}

async function taskRunner(fn, label) {
  const startTime = performance.now();
  console.log(`Task ${label} starting...`);
  let result = await fn();
  console.log(`Task ${label} finished in ${ Number.parseInt(performance.now() - startTime) } miliseconds with,`, result);
}

void taskRunner(series, 'series');
void taskRunner(parallel, 'parallel');


/* 
 * The result will be:
 * Task series starting...
 * Task parallel starting...
 * Task parallel finished in 500 milliseconds with, { "result1": "parallelTask1", "result2": "parallelTask2" }
 * Task series finished in 1001 milliseconds with, { "result1": "seriesTask1", "result2": "seriesTask2" }
 */

Remarque: vous aurez besoin d'un navigateur avec async/awaitactivé pour exécuter cet extrait (ou nodejs v7 et supérieur).

De cette façon, vous pouvez simplement utiliser try/catch pour traiter vos erreurs et renvoyer des résultats partiels dans la fonction parallel.

86
NoNameProvided

Si vous êtes d'accord avec le comportement d'échec rapide de Promise.all et la syntaxe d'attribution de déstructuration:

const [userResponse, postsResponse] = await Promise.all([
  fetchUserAsync(),
  fetchPostsAsync(),
]);
15
ricka

Pour ceux qui demandent comment vous étendez cela jusqu'à un nombre d'appels déterminé à l'exécution, vous pouvez utiliser 2 boucles. Le premier commence toutes les tâches, le second attend que tout soit fini

console.clear();

function wait(ms, data) {
  return new Promise( resolve => setTimeout(resolve.bind(this, data), ms) );
}

/** 
 * While here we call the functions first,
 * then wait for the result later, so 
 * this will finish in 500ms.
 */
async function runTasks(timings) {
  let tasks = [];
  for (let i in timings) {
      tasks.Push(wait(timings[i], `Result of task ${i}`));
  }

  /* Want fast fail? use Promise.All */
  //return Promise.All(tasks);
  
  let results = [];
  for (let task of tasks) {
       results.Push(await task);
  }

  return results;
}

async function taskRunner(fn, arg, label) {
  const startTime = performance.now();
  console.log(`Task ${label} starting...`);
  let result = await fn(arg);
  console.log(`Task ${label} finished in ${ Number.parseInt(performance.now() - startTime) } miliseconds with,`, result);
}

void taskRunner(runTasks, [50,100,200,60,500], 'Task List');

2
Wilco

Le pseudo-code peut être écrit comme suit:

fn async task() {
  result-1 = doAsync();
  result-2 = doAsync();
  result-n = doLongAsync();
  try{
  // handle results together
  combinedResult = handleResults(await result-1, await result-2);
  lastResult = handleLastResult(await result-n);
  }
  catch(err){
   console.error(err)
  }

}

result-1, result-2, result-n s'exécutent en parallèle . 2 sont disponibles et la valeur lastResult, c'est-à-dire que handleLastResult sera renvoyée une fois le résultat-n disponible.

J'espère que cela t'aides

0
Nagaraja Malla

En fait, je viens de faire la même chose. En utilisant promises puis Promise.all pour les synchroniser à la fin, vous pouvez effectuer plusieurs demandes simultanées, mais assurez-vous de disposer de tous les résultats avant de terminer.

Voir ici le dernier exemple: http://javascriptrambling.blogspot.com/2017/04/to-promised-land-with-asyncawait-and.html

0
Kevin Williams

Premièrement, votre code est-il un code bloquant?

Si c'est le cas, rappelez-vous que javascript est un seul thread, vous ne pouvez donc pas exécuter deux codes synchrones, par exemple deux boucles (pour ou pendant) en même temps.

Mais, il est possible de atteindre cela en utilisant Web Workers, j'ai réussi à exécuter des fonctions dans des travailleurs Web génériques sans utiliser de fichiers js séparés.

setInterval(()=>{console.log("non blocked " + Math.random())}, 900)

console.log("start blocking code in parallel in web Worker")
console.time("blocked")

genericWorker(window, ["blockCpu", function (block){    
    block(10000) //This blockCpu function is defined below
    return "\n\nbla bla\n" //This is catched in the resolved promise

}]).then(function (result){
    console.timeEnd("blocked")
    console.log("End of blocking code", result)
})
.catch(function(error) { console.log(error) })


/*  A Web Worker that does not use a File, it create that from a Blob
    @cb_context, The context where the callback functions arguments are, ex: window
    @cb, ["fn_name1", "fn_name2", function (fn1, fn2) {}]
        The callback will be executed, and you can pass other functions to that cb
*/
function genericWorker(cb_context, cb) {
    return new Promise(function (resolve, reject) {

        if (!cb || !Array.isArray(cb))
            return reject("Invalid data")

        var callback = cb.pop()
        var functions = cb

        if (typeof callback != "function" || functions.some((fn)=>{return typeof cb_context[fn] != "function"}))
            return reject(`The callback or some of the parameters: (${functions.toString()}) are not functions`)

        if (functions.length>0 && !cb_context)
            return reject("context is undefined")

        callback = fn_string(callback) //Callback to be executed
        functions = functions.map((fn_name)=> { return fn_string( cb_context[fn_name] ) })

        var worker_file = window.URL.createObjectURL( new Blob(["self.addEventListener('message', function(e) { var bb = {}; var args = []; for (fn of e.data.functions) { bb[fn.name] = new Function(fn.args, fn.body); args.Push(fn.name)}; var callback = new Function( e.data.callback.args, e.data.callback.body); args = args.map(function(fn_name) { return bb[fn_name] });  var result = callback.apply(null, args) ;self.postMessage( result );}, false)"]) )
        var worker = new Worker(worker_file)

        worker.postMessage({ callback: callback, functions: functions })

        worker.addEventListener('error', function(error){ return reject(error.message) })

        worker.addEventListener('message', function(e) {
            resolve(e.data), worker.terminate()
        }, false)

        //From function to string, with its name, arguments and its body
        function fn_string (fn) {
            var name = fn.name, fn = fn.toString()

            return { name: name, 
                args: fn.substring(fn.indexOf("(") + 1, fn.indexOf(")")),
                body: fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}"))
            }
        }
    })
}

//random blocking function
function blockCpu(ms) {
    var now = new Date().getTime(), result = 0
    while(true) {
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
            return;
    }   
}

0
Fernando Carvajal