Idiomatische Möglichkeit, auf mehrere Rückrufe in Node.js zu warten


99

Angenommen, Sie müssen einige Vorgänge ausführen, die von einer temporären Datei abhängen. Da es sich hier um Node handelt, sind diese Vorgänge offensichtlich asynchron. Wie kann man idiomatisch warten, bis alle Vorgänge abgeschlossen sind, um zu wissen, wann die temporäre Datei gelöscht werden kann?

Hier ist ein Code, der zeigt, was ich tun möchte:

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

Wenn ich es aber so schreibe, kann der dritte Aufruf ausgeführt werden, bevor die ersten beiden die Möglichkeit haben, die Datei zu verwenden. Ich brauche eine Möglichkeit, um sicherzustellen, dass die ersten beiden Anrufe bereits beendet sind (ihre Rückrufe wurden aufgerufen), bevor ich fortfahre, ohne die Anrufe zu verschachteln (und sie in der Praxis synchron zu machen).

Ich dachte darüber nach, Ereignissender für die Rückrufe zu verwenden und einen Zähler als Empfänger zu registrieren. Der Zähler würde die fertigen Ereignisse empfangen und zählen, wie viele Operationen noch ausstehen. Wenn der letzte fertig ist, wird die Datei gelöscht. Aber es besteht das Risiko einer Rennbedingung und ich bin mir nicht sicher, wie das normalerweise gemacht wird.

Wie lösen Node-Leute diese Art von Problem?


Vielen Dank für diese Frage, auch ich habe ein ähnliches Problem.
Krishna Shetty

Antworten:


94

Aktualisieren:

Jetzt würde ich raten, einen Blick darauf zu werfen:

  • Versprechen

    Das Promise-Objekt wird für verzögerte und asynchrone Berechnungen verwendet. Ein Versprechen stellt eine Operation dar, die noch nicht abgeschlossen ist, aber in Zukunft erwartet wird.

    Eine beliebte Versprechensbibliothek ist Bluebird . A würde raten, einen Blick darauf zu werfen, warum es verspricht .

    Sie sollten Versprechen verwenden, um dies zu ändern:

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

    Das mögen:

    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");
    });
  • Generatoren: Zum Beispiel über co .

    Generatorbasierte Steuerungsflussgüte für NodeJs und den Browser unter Verwendung von Versprechungen, mit denen Sie nicht blockierenden Code auf nette Weise schreiben können.

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

Wenn ich das richtig verstehe, sollten Sie sich die sehr gute asynchrone Bibliothek ansehen . Sie sollten sich besonders die Serie ansehen . Nur eine Kopie von den Ausschnitten von der Github-Seite:

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

Als Plus kann diese Bibliothek auch im Browser ausgeführt werden.


21
Am Ende habe ich tatsächlich async.parallel verwendet, da die Operationen unabhängig sind und ich sie nicht auf die vorherigen warten lassen wollte.
Thiago Arrais

22

Der einfachste Weg, einen ganzzahligen Zähler zu erhöhen, wenn Sie eine asynchrone Operation starten und dann im Rückruf den Zähler dekrementieren. Abhängig von der Komplexität kann der Rückruf den Zähler auf Null prüfen und dann die Datei löschen.

Etwas komplexer wäre es, eine Liste von Objekten zu führen, und jedes Objekt hätte alle Attribute, die Sie zur Identifizierung der Operation benötigen (es könnte sogar der Funktionsaufruf sein), sowie einen Statuscode. Die Rückrufe würden den Statuscode auf abgeschlossen setzen.

Dann hätten Sie eine Schleife, die wartet (mit process.nextTick) und prüft, ob alle Aufgaben abgeschlossen sind. Der Vorteil dieser Methode gegenüber dem Zähler besteht darin, dass Sie die Datei vorzeitig löschen, wenn alle ausstehenden Aufgaben ausgeführt werden können, bevor alle Aufgaben ausgegeben wurden.


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

Es gibt keine "native" Lösung, aber es gibt eine Million Flusskontrollbibliotheken für Knoten. Vielleicht gefällt Ihnen 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);
  }
)

Oder, wie Michael vorschlug, könnten Zähler eine einfachere Lösung sein. Schauen Sie sich dieses Semaphor-Modell an . Sie würden es so verwenden:

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

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

6

Ich möchte eine andere Lösung anbieten, die die Geschwindigkeit und Effizienz des Programmierparadigmas im Kern von Node nutzt: Ereignisse.

Alles, was Sie mit Versprechungen oder Modulen tun können, die zur Verwaltung der Flusskontrolle entwickelt wurden async, kann mithilfe von Ereignissen und einer einfachen Zustandsmaschine erreicht werden, die meiner Meinung nach eine Methodik bietet, die möglicherweise einfacher zu verstehen ist als andere Optionen.

Angenommen, Sie möchten die Länge mehrerer Dateien parallel summieren:

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

Welches wird ausgegeben:

Start: Beginn der Verarbeitung von 4 Pfaden
forEachPath: Starten von 4 Prozessketten
  readPath: file1
  readPath: file2
  processContent: file1
  readPath: file3
  processContent: file2
  processContent: file3
  readPath: file4
  processContent: file4
Die Gesamtzahl von 4 Dateien beträgt 4021

Beachten Sie, dass die Reihenfolge der Prozesskettenaufgaben von der Systemlast abhängt.

Sie können sich den Programmablauf folgendermaßen vorstellen:

start -> forEachPath - + -> readPath 1 -> processContent 1 - + -> done
                      + -> readFile 2 -> processContent 2 - +
                      + -> readFile 3 -> processContent 3 - +
                      + -> readFile 4 -> processContent 4 - +

Für die Wiederverwendung wäre es trivial, ein Modul zu erstellen, das die verschiedenen Flusssteuerungsmuster unterstützt, dh Serien, Parallel, Batch, während, bis usw.


2

Die einfachste Lösung besteht darin, do_something * auszuführen und die Verknüpfung wie folgt aufzuheben:

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

Sofern Sie aus Leistungsgründen do_something () und do_something_other () nicht parallel ausführen möchten, empfehle ich, es einfach zu halten und diesen Weg zu gehen.



1

Mit reinen Versprechungen könnte es etwas chaotischer sein, aber wenn Sie verzögerte Versprechungen verwenden, ist es nicht so schlimm:

Installieren:

npm install --save @bitbar/deferred-promise

Ändern Sie Ihren Code:

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);
});
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.