Как запустить async / await параллельно в Javascript

наконец-то async/await будет поддерживает во всех основных браузерах, кроме IE. Итак, теперь мы можем начать писать более читаемый код с помощью async/await но есть одна загвоздка. Многие люди используют async await следующим образом:

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

в то время как этот код читается, у него есть проблема, он запускает функции последовательно, он не начнет выборку сообщений до тех пор, пока выборка пользователя не будет завершена. Решения просты, нам нужно получить ресурсы в параллельный.

Итак, что я хочу сделать, это (псевдо языке):

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

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

  lastResult = handleLastResult(result-n);
}

6 ответов


вы можете писать что-то вроде этого:

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

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

это легко, не так ли? Но есть одна загвоздка. Promise.all и fail-быстрое поведение это означает, что он отвергнет, как только одно из обещаний будет отвергнуто. Вероятно, вам нужно более надежное решение, в котором мы отвечаем за обработку отклонений любой из выборки. К счастью, есть решение, это может быть достигнуто просто с async/await без необходимости использования Promise.all. Рабочий пример:

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

Примечание: вам понадобится браузер, который имеет async/await включено для запуска этого фрагмента (или nodejs v7 и выше)

таким образом, вы можете использовать просто try/ catch для обработки ваших ошибок и возврата частичных результатов внутри


Если вы в порядке с быстрым поведением обещания.все и реструктуризующее присваивание синтаксис:

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

Я на самом деле сделал то же самое. Используя обещания, а затем обещания.чтобы синхронизировать их в конце, вы можете выполнить много параллельных запросов, но затем убедитесь, что у вас есть все результаты до завершения.

см. здесь в последнем примере: http://javascriptrambling.blogspot.com/2017/04/to-promised-land-with-asyncawait-and.html


псевдо-код можно записать следующим образом:

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 будет работать параллельно. combinedResult и lastResult также будут работать параллельно. Однако combinedResult значение, т. е. возвращение функции handleResults будет возвращен результат-1 и 2 доступны и значение lastResult я.е handleLastResult будет возвращен результат-N доступен.

надеюсь, что это помогает


во-первых, ваш код блокировки-код?

если да, помните, что javascript-это один поток, поэтому вы не можете запускать два синхронных кода, например два цикла (for или while) одновременно.

но, возможно достичь этого с помощью веб-работников, мне удалось выполнить функции в generic web workers и без использования отдельных JS-файлов.

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

для тех, кто спрашивает, как вы расширяете это до определенного количества вызовов во время выполнения, вы можете использовать 2 цикла. Первый запускает все задания, второй ждет, пока все закончится

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