Coordinando la ejecución paralela en node.js

79

El modelo de programación dirigido por eventos de node.js hace que sea algo complicado coordinar el flujo del programa.

La ejecución secuencial simple se convierte en devoluciones de llamada anidadas, lo cual es bastante fácil (aunque un poco complicado de escribir).

Pero, ¿qué hay de la ejecución paralela? Supongamos que tiene tres tareas A, B, C que pueden ejecutarse en paralelo y, cuando estén terminadas, desea enviar sus resultados a la tarea D.

Con un modelo de bifurcación / unión, esto sería

  • tenedor A
  • tenedor B
  • tenedor C
  • unirse a A, B, C, ejecutar D

¿Cómo escribo eso en node.js? ¿Existen mejores prácticas o libros de cocina? ¿Tengo que poner a mano una solución cada vez, o hay alguna biblioteca con ayuda para esto?

Thilo
fuente

Respuestas:

128

Nada es realmente paralelo en node.js, ya que es de un solo subproceso. Sin embargo, se pueden programar y ejecutar varios eventos en una secuencia que no se puede determinar de antemano. Y algunas cosas, como el acceso a la base de datos, son en realidad "paralelas" en el sentido de que las consultas de la base de datos se ejecutan en subprocesos separados pero se vuelven a integrar en el flujo de eventos cuando se completan.

Entonces, ¿cómo se programa una devolución de llamada en varios controladores de eventos? Bueno, esta es una técnica común usada en animaciones en javascript del lado del navegador: use una variable para rastrear la finalización.

Esto suena como un truco y lo es, y suena potencialmente desordenado dejando un montón de variables globales alrededor del seguimiento y en un lenguaje menor sería. Pero en javascript podemos usar cierres:

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var callback = function () {
    counter --;
    if (counter == 0) {
      shared_callback()
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](callback);
  }
}

// usage:
fork([A,B,C],D);

En el ejemplo anterior, mantenemos el código simple asumiendo que las funciones async y callback no requieren argumentos. Por supuesto, puede modificar el código para pasar argumentos a las funciones asíncronas y hacer que la función de devolución de llamada acumule resultados y los pase a la función de devolución de llamada compartida.


Respuesta adicional:

En realidad, incluso tal como está, esa fork()función ya puede pasar argumentos a las funciones asíncronas usando un cierre:

fork([
  function(callback){ A(1,2,callback) },
  function(callback){ B(1,callback) },
  function(callback){ C(1,2,callback) }
],D);

lo único que queda por hacer es acumular los resultados de A, B, C y pasarlos a D.


Aún más respuesta adicional:

No pude resistir. Seguí pensando en esto durante el desayuno. Aquí hay una implementación de fork()que acumula resultados (generalmente pasados ​​como argumentos a la función de devolución de llamada):

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var all_results = [];
  function makeCallback (index) {
    return function () {
      counter --;
      var results = [];
      // we use the arguments object here because some callbacks 
      // in Node pass in multiple arguments as result.
      for (var i=0;i<arguments.length;i++) {
        results.push(arguments[i]);
      }
      all_results[index] = results;
      if (counter == 0) {
        shared_callback(all_results);
      }
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](makeCallback(i));
  }
}

Eso fue bastante fácil. Esto tiene fork()un propósito bastante general y se puede utilizar para sincronizar múltiples eventos no homogéneos.

Ejemplo de uso en Node.js:

// Read 3 files in parallel and process them together:

function A (c){ fs.readFile('file1',c) };
function B (c){ fs.readFile('file2',c) };
function C (c){ fs.readFile('file3',c) };
function D (result) {
  file1data = result[0][1];
  file2data = result[1][1];
  file3data = result[2][1];

  // process the files together here
}

fork([A,B,C],D);

Actualizar

Este código fue escrito antes de la existencia de bibliotecas como async.js o las diversas bibliotecas basadas en promesas. Me gustaría creer que async.js se inspiró en esto, pero no tengo ninguna prueba de ello. De todos modos ... si estás pensando en hacer esto hoy, echa un vistazo a async.js o promesas. Solo considere la respuesta anterior como una buena explicación / ilustración de cómo funcionan cosas como async.parallel.

En aras de la integridad, lo siguiente es cómo lo haría con async.parallel:

var async = require('async');

async.parallel([A,B,C],D);

Tenga en cuenta que async.parallelfunciona exactamente igual que la forkfunción que implementamos anteriormente. La principal diferencia es que pasa un error como primer argumento Dy la devolución de llamada como segundo argumento según la convención de node.js.

Usando promesas, lo escribiríamos de la siguiente manera:

// Assuming A, B & C return a promise instead of accepting a callback

Promise.all([A,B,C]).then(D);
slebetman
fuente
12
"Nada es realmente paralelo en node.js, ya que es de un solo subproceso". No es verdad. Todo lo que no usa la CPU (como esperar la E / S de la red) se ejecuta en paralelo.
Thilo
3
Es cierto, en su mayor parte. Esperar IO en Node no bloquea la ejecución de otro código, pero cuando se ejecuta el código, es de uno en uno. La única ejecución paralela verdadera en Node es la generación de procesos secundarios, pero eso podría decirse de casi cualquier entorno.
MooGoo
6
@Thilo: Por lo general, llamamos al código que no usa la CPU como no se está ejecutando. Si no está ejecutando, no puede "ejecutar" en paralelo.
slebetman
4
@MooGoo: La implicación de esto es que con los eventos, porque sabemos que definitivamente no pueden ejecutarse en paralelo, no tenemos que preocuparnos por los semáforos y mutex, mientras que con los subprocesos tenemos que bloquear los recursos compartidos.
slebetman
2
¿Estoy en lo correcto al decir que estas no son funciones que se ejecutan en paralelo, pero que (en el mejor de los casos) se ejecutan en una secuencia indeterminada con el código que no progresa hasta que regresa cada 'async_func'?
Aaron Rustad
10

Creo que ahora el módulo "async" proporciona esta funcionalidad paralela y es aproximadamente la misma que la función fork anterior.

Wes Gamble
fuente
2
Esto es incorrecto, async solo lo ayuda a organizar el flujo de su código dentro de un solo proceso.
bwindels
2
async.parallel de hecho hace aproximadamente lo mismo que la forkfunción anterior
Dave Stibrany
no es un verdadero paralelismo
rab
5

El módulo de futuros tiene un submódulo llamado unión que me ha gustado usar:

Une llamadas asincrónicas de forma similar a como pthread_join funciona para subprocesos.

El archivo Léame muestra algunos buenos ejemplos de cómo usarlo en estilo libre o usar el submódulo futuro usando el patrón Promise. Ejemplo de los documentos:

var Join = require('join')
  , join = Join()
  , callbackA = join.add()
  , callbackB = join.add()
  , callbackC = join.add();

function abcComplete(aArgs, bArgs, cArgs) {
  console.log(aArgs[1] + bArgs[1] + cArgs[1]);
}

setTimeout(function () {
  callbackA(null, 'Hello');
}, 300);

setTimeout(function () {
  callbackB(null, 'World');
}, 500);

setTimeout(function () {
  callbackC(null, '!');
}, 400);

// this must be called after all 
join.when(abcComplete);
Cachondo
fuente
2

Una solución simple podría ser posible aquí: http://howtonode.org/control-flow-part-ii desplácese hasta Acciones paralelas. Otra forma sería hacer que A, B y C compartan la misma función de devolución de llamada, que la función tenga un incremento global o al menos fuera de la función, si los tres han llamado a la devolución de llamada, déjela ejecutar D, por supuesto, también tendrá que almacenar los resultados de A, B y C en algún lugar.

Alex
fuente
0

Además de las promesas populares y la biblioteca asíncrona, existe una tercera forma elegante: usar "cableado":

var l = new Wire();

funcA(l.branch('post'));
funcB(l.branch('comments'));
funcC(l.branch('links'));

l.success(function(results) {
   // result will be object with results:
   // { post: ..., comments: ..., links: ...}
});

https://github.com/garmoshka-mo/mo-wire

Daniel Garmoshka
fuente