Llame a funciones asíncronas / en espera en paralelo

434

Por lo que yo entiendo, en ES7 / ES2016, poner múltiples await's en el código funcionará de manera similar a encadenar .then()con promesas, lo que significa que se ejecutarán uno tras otro en lugar de en parallerl. Entonces, por ejemplo, tenemos este código:

await someCall();
await anotherCall();

¿Entiendo correctamente que anotherCall()se llamará solo cuando someCall()se complete? ¿Cuál es la forma más elegante de llamarlos en paralelo?

Quiero usarlo en Node, ¿tal vez hay una solución con la biblioteca asíncrona?

EDITAR: no estoy satisfecho con la solución proporcionada en esta pregunta: ralentización debido a la espera no paralela de promesas en generadores asíncronos , porque usa generadores y estoy preguntando sobre un caso de uso más general.

Victor Marchuk
fuente
1
@adeneo Eso es incorrecto, Javascript nunca se ejecuta en paralelo dentro de su propio contexto.
Blindman67
55
@ Blindman67: lo hace, al menos de la manera en que el OP significa, donde dos operaciones asíncronas se ejecutan simultáneamente, pero no en este caso, lo que quise escribir era que se ejecutaran en serie , la primera awaitesperaría a que se completara la primera función completamente antes de ejecutar el segundo.
adeneo
3
@ Blindman67: es de un solo subproceso, pero esa limitación no se aplica a los métodos asíncronos, pueden ejecutarse simultáneamente y devolver la respuesta cuando terminan, es decir, lo que el OP significa "paralelo".
adeneo
77
@ Blindman67: creo que está bastante claro lo que está pidiendo el OP, el uso del patrón asíncrono / espera hará que las funciones se ejecuten en serie, incluso si son asíncronas, por lo que el primero finalizará por completo antes de que se llame al segundo, etc. preguntando cómo llamar a ambas funciones en paralelo, y como son claramente asíncronas, el objetivo es ejecutarlas simultáneamente, es decir, en paralelo, por ejemplo, haciendo dos solicitudes ajax simultáneamente, lo que no es un problema en javascript, como la mayoría de los métodos asíncronos , como has notado, ejecuta código nativo y usa más hilos.
adeneo
3
@Bergi, esto no es un duplicado de la pregunta vinculada: se trata específicamente de la sintaxis async / wait y Native PromiseS. La pregunta vinculada se refiere a la biblioteca de bluebird con generadores y rendimiento. Conceptualmente similar quizás, pero no en implementación.
Iest

Respuestas:

703

Puedes esperar en Promise.all():

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

Para almacenar los resultados:

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

Tenga en cuenta que Promise.allfalla rápidamente, lo que significa que tan pronto como una de las promesas que se le suministran rechaza, todo lo rechaza.

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, en cambio, desea esperar a que se cumplan o rechacen todas las promesas, puede usarlas Promise.allSettled. Tenga en cuenta que Internet Explorer no admite de forma nativa este método.

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
fuente
79
Limpie pero tenga en cuenta el comportamiento de falla rápida de Promise.all. Si alguna de las funciones arroja un error, Promise.all rechazará
NoNameProvided
11
Puede manejar bien los resultados parciales con async /
await
131
Consejo profesional: use la desestructuración de matrices para inicializar un número arbitrario de resultados de Promise.all (), como:[result1, result2] = Promise.all([async1(), async2()]);
jonny
10
@jonny ¿Está sujeto a fallas rápidas? Además, ¿todavía se necesita = await Promise.all?
theUtherSide
55
@theUtherSide Tienes toda la razón: no incluí la espera.
jonny
114

TL; DR

Utilice Promise.allpara las llamadas a funciones paralelas, los comportamientos de respuesta no son correctos cuando se produce el error.


Primero, ejecute todas las llamadas asincrónicas a la vez y obtenga todos los Promiseobjetos. En segundo lugar, utilizar awaiten los Promiseobjetos. De esta manera, mientras espera a que el primero se Promiseresuelva, las otras llamadas asincrónicas siguen progresando. En general, solo esperará tanto como la llamada asincrónica más lenta. Por ejemplo:

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

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

Advertencia: no importa si las awaitllamadas están en la misma línea o en líneas diferentes, siempre y cuando la primera awaitllamada ocurra después de todas las llamadas asincrónicas. Ver el comentario de JohnnyHK.


Actualización: esta respuesta tiene un tiempo diferente en el manejo de errores de acuerdo con la respuesta de @ bergi , NO arroja el error a medida que ocurre el error, pero después de que se ejecutan todas las promesas. Comparo el resultado con el consejo de @ jonny: [result1, result2] = Promise.all([async1(), async2()])verifique el siguiente fragmento de código

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

Refugio
fuente
11
Esto me parece una opción mucho más agradable que Promise.all, y con la asignación de desestructuración, incluso puedes hacerlo [someResult, anotherResult] = [await someResult, await anotherResult]si cambias consta let.
jawj
28
Pero esto todavía ejecuta las awaitdeclaraciones en serie, ¿verdad? Es decir, la ejecución se detiene hasta que se awaitresuelve el primero , luego pasa al segundo. Promise.allSe ejecuta en paralelo.
Andru
8
Gracias @Haven. Esta debería ser la respuesta aceptada.
Stefan D
87
Esta respuesta es engañosa ya que el hecho de que ambas esperas se hagan en la misma línea es irrelevante. Lo importante es que las dos llamadas asíncronas se realicen antes de que se espere.
JohnnyHK
15
@Haven esta solución no es lo mismo que Promise.all. Si cada solicitud es una llamada de red, await someResultdeberá resolverse await anotherResultincluso antes de comenzar. Por el contrario, en Promise.alllas dos awaitllamadas se puede iniciar antes de que se resuelva una.
Ben Winding
89

Actualizar:

La respuesta original hace que sea difícil (y en algunos casos imposible) manejar correctamente los rechazos de promesas. La solución correcta es usar Promise.all:

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

Respuesta original:

Solo asegúrese de llamar a ambas funciones antes de esperar una:

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

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
fuente
1
@JeffFischer Agregué comentarios que espero lo aclaren.
Jonathan Potter el
99
Siento que esta es ciertamente la respuesta más pura
Gershom
1
Esta respuesta es mucho más clara que la de Haven. Está claro que las llamadas a funciones devolverán objetos de promesa y awaitluego los resolverán en valores reales.
user1032613
3
Esto parece funcionar a simple vista, pero tiene problemas horribles con los rechazos no controlados . ¡No uses esto!
Bergi
1
@ Bergi Tienes razón, ¡gracias por señalarlo! He actualizado la respuesta con una mejor solución.
Jonathan Potter
24

Hay otra forma sin Promise.all () para hacerlo en paralelo:

Primero, tenemos 2 funciones para imprimir números:

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

Esto es secuencial:

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

Esto es paralelo:

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

Esto se puede lograr con Promise.allSettled () , que es similar Promise.all()pero sin el comportamiento de falla rápida.

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

Nota : Esta es una característica de última generación con compatibilidad limitada con el navegador, por lo que recomiendo incluir un polyfill para esta función.

Jonathan Sudiaman
fuente
7

He creado una esencia que prueba algunas formas diferentes de resolver promesas, con resultados. Puede ser útil ver las opciones que funcionan.

SkarXa
fuente
Las pruebas 4 y 6 en la esencia devolvieron los resultados esperados. Consulte stackoverflow.com/a/42158854/5683904 por NoNameProvided, quien explica la diferencia entre las opciones.
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
    */

Si bien establecer p1, p2 y p3 no los ejecuta estrictamente en paralelo, no retrasan ninguna ejecución y puede atrapar errores contextuales con un catch.

Thrunobulax
fuente
2
Bienvenido a Stack Overflow. Si bien su código puede proporcionar la respuesta a la pregunta, agregue contexto a su alrededor para que otros tengan una idea de lo que hace y por qué está allí.
Theo
1

En mi caso, tengo varias tareas que quiero ejecutar en paralelo, pero necesito hacer algo diferente con el resultado de esas tareas.

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

Y la salida:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
fuente
genial para la creación dinámica (variedad de recursos)
Michal Miky Jankovský
1

aguarde Promise.all ([someCall (), anotherCall ()]); como ya se mencionó, actuará como una cerca de hilos (muy común en código paralelo como CUDA), por lo tanto, permitirá que todas las promesas se ejecuten sin bloquearse entre sí, pero evitará que la ejecución continúe hasta que TODOS se resuelvan.

Otro enfoque que vale la pena compartir es el asíncrono Node.js que también le permitirá controlar fácilmente la cantidad de concurrencia que generalmente es deseable si la tarea está directamente vinculada al uso de recursos limitados como llamadas API, operaciones de 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éditos para el autor del artículo medio ( leer más )

Thiago Conrado
fuente
-5

Yo voto por:

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

Tenga en cuenta el momento en que llama a las funciones, puede causar resultados inesperados:

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

Pero el siguiente siempre activa la solicitud para crear un nuevo usuario

// 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
fuente
Como declaraste la función fuera / antes de la prueba de condición, y las llamaste. Intenta envolverlos en elsebloque.
Haven
@Haven: quiero decir que cuando separas los momentos en que llamas a las funciones frente a las esperas, puedes obtener resultados inesperados, por ejemplo: solicitudes HTTP asíncronas.
Hoang Le Anh Tu
-6

Creo una función auxiliar waitAll, puede ser que la haga más dulce. Solo funciona en nodejs por ahora, no en el navegador Chrome.

    //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
fuente
3
No, la paralelización no está sucediendo en absoluto aquí. El forciclo espera secuencialmente cada promesa y agrega el resultado a la matriz.
Szczepan Hołyszewski
Entiendo que esto parece no funcionar para las personas. Así que probé en node.js y navegador. La prueba se pasa en node.js (v10, v11), firefox, no funciona en el navegador Chrome. El caso de prueba está en gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang
2
Me niego a creer esto. No hay nada en el estándar que diga que diferentes iteraciones de un bucle for se puedan paralelizar automáticamente; Así no es como funciona JavaScript. La forma en que se escribe el código del bucle, significa esto: "esperar un elemento (el expr de espera), ENTONCES empujar el resultado a temp, ENTONCES tomar el siguiente elemento (siguiente iteración del bucle for). El" en espera "para cada elemento es completamente confinado a una sola iteración del bucle. Si las pruebas muestran que hay paralelización, debe ser porque el transpilador está haciendo algo no estándar o está
lleno de
@ SzczepanHołyszewski Su confianza en disimular sin ejecutar el caso de prueba me inspira a hacer algunos cambios de nombre y comentarios adicionales. Todo el código es simple y antiguo ES6, no se requiere transpilación.
Fred Yang
No estoy seguro de por qué esto se rechaza tanto. Es esencialmente la misma respuesta que @ user2883596 dio.
Jonathan Sudiaman