Manera idiomática de esperar múltiples devoluciones de llamada en Node.js


99

Suponga que necesita realizar algunas operaciones que dependen de algún archivo temporal. Ya que estamos hablando de Node aquí, esas operaciones son obviamente asincrónicas. ¿Cuál es la forma idiomática de esperar a que finalicen todas las operaciones para saber cuándo se puede eliminar el archivo temporal?

Aquí hay un código que muestra lo que quiero hacer:

do_something(tmp_file_name, function(err) {});
do_something_other(tmp_file_name, function(err) {});
fs.unlink(tmp_file_name);

Pero si lo escribo de esta manera, la tercera llamada se puede ejecutar antes de que las dos primeras tengan la oportunidad de usar el archivo. Necesito alguna forma de garantizar que las dos primeras llamadas ya terminaron (invocaron sus devoluciones de llamada) antes de continuar sin anidar las llamadas (y hacerlas sincrónicas en la práctica).

Pensé en usar emisores de eventos en las devoluciones de llamada y registrar un contador como receptor. El contador recibiría los eventos terminados y contabilizaría cuántas operaciones quedaban pendientes. Cuando terminara el último, eliminaría el archivo. Pero existe el riesgo de una condición de carrera y no estoy seguro de que sea así como se hace habitualmente.

¿Cómo resuelve la gente de Node este tipo de problema?


Gracias por esta pregunta, yo también tengo un problema similar.
Krishna Shetty

Respuestas:


94

Actualizar:

Ahora aconsejaría echar un vistazo a:

  • Promesas

    El objeto Promise se utiliza para cálculos diferidos y asincrónicos. Una promesa representa una operación que aún no se ha completado, pero que se espera en el futuro.

    Una biblioteca popular de promesas es bluebird . A aconsejaría echar un vistazo a por qué las promesas .

    Deberías usar promesas para convertir esto:

    fs.readFile("file.json", function (err, val) {
        if (err) {
            console.error("unable to read file");
        }
        else {
            try {
                val = JSON.parse(val);
                console.log(val.success);
            }
            catch (e) {
                console.error("invalid json in file");
            }
        }
    });

    Dentro de esto:

    fs.readFileAsync("file.json").then(JSON.parse).then(function (val) {
        console.log(val.success);
    })
    .catch(SyntaxError, function (e) {
        console.error("invalid json in file");
    })
    .catch(function (e) {
        console.error("unable to read file");
    });
  • generadores: por ejemplo a través de co .

    Bondad de flujo de control basado en generador para nodejs y el navegador, usando promesas, lo que le permite escribir código sin bloqueo de una manera agradable.

    var co = require('co');
    
    co(function *(){
      // yield any promise
      var result = yield Promise.resolve(true);
    }).catch(onerror);
    
    co(function *(){
      // resolve multiple promises in parallel
      var a = Promise.resolve(1);
      var b = Promise.resolve(2);
      var c = Promise.resolve(3);
      var res = yield [a, b, c];
      console.log(res);
      // => [1, 2, 3]
    }).catch(onerror);
    
    // errors can be try/catched
    co(function *(){
      try {
        yield Promise.reject(new Error('boom'));
      } catch (err) {
        console.error(err.message); // "boom"
     }
    }).catch(onerror);
    
    function onerror(err) {
      // log any uncaught errors
      // co will not throw any errors you do not handle!!!
      // HANDLE ALL YOUR ERRORS!!!
      console.error(err.stack);
    }

Si entiendo correctamente, creo que deberías echar un vistazo a la muy buena biblioteca asíncrona . Especialmente deberías echar un vistazo a la serie . Solo una copia de los fragmentos de la página de github:

async.series([
    function(callback){
        // do some stuff ...
        callback(null, 'one');
    },
    function(callback){
        // do some more stuff ...
        callback(null, 'two');
    },
],
// optional callback
function(err, results){
    // results is now equal to ['one', 'two']
});


// an example using an object instead of an array
async.series({
    one: function(callback){
        setTimeout(function(){
            callback(null, 1);
        }, 200);
    },
    two: function(callback){
        setTimeout(function(){
            callback(null, 2);
        }, 100);
    },
},
function(err, results) {
    // results is now equals to: {one: 1, two: 2}
});

Además, esta biblioteca también se puede ejecutar en el navegador.


21
De hecho, terminé usando async.parallel, ya que las operaciones son independientes y no quería hacerlas esperar en las anteriores.
Thiago Arrais

22

La forma más sencilla de incrementar un contador de enteros cuando inicia una operación asíncrona y luego, en la devolución de llamada, reduce el contador. Dependiendo de la complejidad, la devolución de llamada podría verificar si el contador es cero y luego eliminar el archivo.

Un poco más complejo sería mantener una lista de objetos, y cada objeto tendría los atributos que necesita para identificar la operación (incluso podría ser la llamada a la función), así como un código de estado. Las devoluciones de llamada establecerían el código de estado como completado.

Entonces tendrías un ciclo que espera (usando process.nextTick) y verifica si todas las tareas están completadas. La ventaja de este método de venta libre es que si es posible completar todas las tareas pendientes, antes de que se emitan todas las tareas, la técnica del contador provocaría que elimine el archivo antes de tiempo.


11
// simple countdown latch
function CDL(countdown, completion) {
    this.signal = function() { 
        if(--countdown < 1) completion(); 
    };
}

// usage
var latch = new CDL(10, function() {
    console.log("latch.signal() was called 10 times.");
});

7

No existe una solución "nativa", pero hay un millón de bibliotecas de control de flujo para node. Puede que te guste Step:

Step(
  function(){
      do_something(tmp_file_name, this.parallel());
      do_something_else(tmp_file_name, this.parallel());
  },
  function(err) {
    if (err) throw err;
    fs.unlink(tmp_file_name);
  }
)

O, como sugirió Michael, los contadores podrían ser una solución más simple. Eche un vistazo a esta maqueta de semáforo . Lo usarías así:

do_something1(file, queue('myqueue'));
do_something2(file, queue('myqueue'));

queue.done('myqueue', function(){
  fs.unlink(file);
});

6

Me gustaría ofrecer otra solución que utiliza la velocidad y la eficiencia del paradigma de programación en el núcleo mismo de Node: events.

Todo lo que puede hacer con Promesas o módulos diseñados para administrar el control de flujo, como async, se puede lograr usando eventos y una máquina de estado simple, que creo que ofrece una metodología que es, quizás, más fácil de entender que otras opciones.

Por ejemplo, suponga que desea sumar la longitud de varios archivos en paralelo:

const EventEmitter = require('events').EventEmitter;

// simple event-driven state machine
const sm = new EventEmitter();

// running state
let context={
  tasks:    0,    // number of total tasks
  active:   0,    // number of active tasks
  results:  []    // task results
};

const next = (result) => { // must be called when each task chain completes

  if(result) { // preserve result of task chain
    context.results.push(result);
  }

  // decrement the number of running tasks
  context.active -= 1; 

  // when all tasks complete, trigger done state
  if(!context.active) { 
    sm.emit('done');
  }
};

// operational states
// start state - initializes context
sm.on('start', (paths) => {
  const len=paths.length;

  console.log(`start: beginning processing of ${len} paths`);

  context.tasks = len;              // total number of tasks
  context.active = len;             // number of active tasks

  sm.emit('forEachPath', paths);    // go to next state
});

// start processing of each path
sm.on('forEachPath', (paths)=>{

  console.log(`forEachPath: starting ${paths.length} process chains`);

  paths.forEach((path) => sm.emit('readPath', path));
});

// read contents from path
sm.on('readPath', (path) => {

  console.log(`  readPath: ${path}`);

  fs.readFile(path,(err,buf) => {
    if(err) {
      sm.emit('error',err);
      return;
    }
    sm.emit('processContent', buf.toString(), path);
  });

});

// compute length of path contents
sm.on('processContent', (str, path) => {

  console.log(`  processContent: ${path}`);

  next(str.length);
});

// when processing is complete
sm.on('done', () => { 
  const total = context.results.reduce((sum,n) => sum + n);
  console.log(`The total of ${context.tasks} files is ${total}`);
});

// error state
sm.on('error', (err) => { throw err; });

// ======================================================
// start processing - ok, let's go
// ======================================================
sm.emit('start', ['file1','file2','file3','file4']);

Que dará como resultado:

inicio: inicio del procesamiento de 4 rutas
forEachPath: iniciando 4 cadenas de procesos
  readPath: file1
  readPath: file2
  processContent: file1
  readPath: file3
  processContent: file2
  processContent: file3
  readPath: file4
  processContent: file4
El total de 4 archivos es 4021

Tenga en cuenta que el orden de las tareas de la cadena de procesos depende de la carga del sistema.

Puede visualizar el flujo del programa como:

inicio -> forEachPath - + -> readPath 1 -> processContent 1 - + -> hecho
                      + -> readFile 2 -> processContent 2 - +
                      + -> readFile 3 -> processContent 3 - +
                      + -> readFile 4 -> processContent 4 - +

Para su reutilización, sería trivial crear un módulo que admita los diversos patrones de control de flujo, es decir, serie, paralelo, lote, mientras, hasta, etc.


2

La solución más simple es ejecutar do_something * y desvincular en secuencia de la siguiente manera:

do_something(tmp_file_name, function(err) {
    do_something_other(tmp_file_name, function(err) {
        fs.unlink(tmp_file_name);
    });
});

A menos que, por razones de rendimiento, desee ejecutar do_something () y do_something_other () en paralelo, sugiero que sea simple y siga este camino.



1

Con Promesas puras, podría ser un poco más complicado, pero si usa Promesas diferidas, entonces no es tan malo:

Instalar en pc:

npm install --save @bitbar/deferred-promise

Modifica tu código:

const DeferredPromise = require('@bitbar/deferred-promise');

const promises = [
  new DeferredPromise(),
  new DeferredPromise()
];

do_something(tmp_file_name, (err) => {
  if (err) {
    promises[0].reject(err);
  } else {
    promises[0].resolve();
  }
});

do_something_other(tmp_file_name, (err) => {
  if (err) {
    promises[1].reject(err);
  } else {
    promises[1].resolve();
  }
});

Promise.all(promises).then( () => {
  fs.unlink(tmp_file_name);
});
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.