Idiomatyczny sposób oczekiwania na wiele wywołań zwrotnych w Node.js.


99

Załóżmy, że musisz wykonać pewne operacje, które zależą od jakiegoś pliku tymczasowego. Ponieważ mówimy tutaj o Node, te operacje są oczywiście asynchroniczne. Jaki jest idiomatyczny sposób czekania na zakończenie wszystkich operacji, aby wiedzieć, kiedy można usunąć plik tymczasowy?

Oto kod pokazujący, co chcę zrobić:

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

Ale jeśli napiszę to w ten sposób, trzecie wywołanie może zostać wykonane, zanim pierwsze dwa będą miały szansę użyć pliku. Potrzebuję jakiegoś sposobu, aby zagwarantować, że pierwsze dwa wywołania już się zakończyły (wywołały ich wywołania zwrotne) przed przejściem dalej bez ich zagnieżdżania (i uczynienia ich synchronicznymi w praktyce).

Myślałem o użyciu emiterów zdarzeń na callbackach i zarejestrowaniu licznika jako odbiornika. Licznik otrzymywałby zakończone zdarzenia i liczył, ile operacji jest jeszcze w toku. Kiedy ostatni skończył, usuwał plik. Ale istnieje ryzyko wyścigu i nie jestem pewien, czy zwykle tak się to robi.

Jak ludzie korzystający z węzłów rozwiązują tego rodzaju problemy?


Dzięki za to pytanie, ja też mam podobny problem.
— Krishna Shetty

Odpowiedzi:


94

Aktualizacja:

Teraz radziłbym rzucić okiem na:

  • Obietnice

    Obiekt Promise służy do odroczonych i asynchronicznych obliczeń. Obietnica reprezentuje operację, która jeszcze się nie zakończyła, ale jest oczekiwana w przyszłości.

    Popularną biblioteką obietnic jest bluebird . Radziłbym przyjrzeć się, dlaczego obiecuje .

    Powinieneś użyć obietnic, aby to zmienić:

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

    Zaangażowany w to:

    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");
    });
    
  • generatory: na przykład przez co .

    Oparty na generatorze przepływ sterowania dla nodejs i przeglądarki, wykorzystujący obietnice, umożliwiający pisanie nieblokującego kodu w przyjemny sposób.

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

Jeśli dobrze rozumiem, myślę, że powinieneś rzucić okiem na bardzo dobrą bibliotekę asynchroniczną . Szczególnie powinieneś przyjrzeć się serii . Tylko kopia fragmentów ze strony 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}
});

Dodatkowo ta biblioteka może działać również w przeglądarce.


21
Skończyło się na używaniu async.parallel, ponieważ operacje są niezależne i nie chciałem, aby czekały na poprzednie.
— Thiago Arrais

22

Najprostszym sposobem jest zwiększanie licznika całkowitoliczbowego, gdy rozpoczynasz operację asynchroniczną, a następnie w wywołaniu zwrotnym zmniejszasz licznik. W zależności od złożoności wywołanie zwrotne może sprawdzić licznik pod kątem zera, a następnie usunąć plik.

Nieco bardziej skomplikowane byłoby utrzymanie listy obiektów, a każdy obiekt miałby atrybuty potrzebne do identyfikacji operacji (może to być nawet wywołanie funkcji), a także kod statusu. Wywołania zwrotne ustawiłyby kod stanu na zakończony.

Wtedy miałbyś pętlę, która czeka (używając process.nextTick) i sprawdza, czy wszystkie zadania są zakończone. Zaletą tej metody w porównaniu z licznikiem jest to, że jeśli możliwe jest ukończenie wszystkich zaległych zadań, zanim wszystkie zadania zostaną wydane, technika licznika spowoduje przedwczesne usunięcie pliku.


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

Nie ma rozwiązania „natywnego”, ale istnieje milion bibliotek sterowania przepływem dla węzła. Możesz polubić 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);
  }
)

Lub, jak zasugerował Michael, prostszym rozwiązaniem mogą być liczniki. Spójrz na tę makietę semafora . Używałbyś tego w ten sposób:

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

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

6

Chciałbym zaoferować inne rozwiązanie, które wykorzystuje szybkość i wydajność paradygmatu programowania w samym rdzeniu Node: events.

Wszystko, co możesz zrobić z Obietnicami lub modułami zaprojektowanymi do zarządzania kontrolą przepływu, na przykład async, można osiągnąć za pomocą zdarzeń i prostego automatu stanu, który, jak sądzę, oferuje metodologię, która jest być może łatwiejsza do zrozumienia niż inne opcje.

Na przykład załóżmy, że chcesz zsumować długość wielu plików równolegle:

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

Który wyświetli:

start: rozpoczęcie przetwarzania 4 ścieżek
forEachPath: uruchomienie 4 łańcuchów procesów
  readPath: plik1
  readPath: plik2
  processContent: plik1
  readPath: plik3
  processContent: plik2
  processContent: plik3
  readPath: plik4
  processContent: plik4
Łącznie 4 pliki to 4021

Należy zauważyć, że kolejność zadań w łańcuchu procesów zależy od obciążenia systemu.

Możesz wyobrazić sobie przebieg programu jako:

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

W przypadku ponownego wykorzystania byłoby trywialne utworzenie modułu obsługującego różne wzorce kontroli przepływu, tj. Szeregowe, równoległe, wsadowe, podczas, do itp.


2

Najprostszym rozwiązaniem jest uruchomienie funkcji do_something * i rozłączenie w następującej kolejności:

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

O ile ze względów wydajnościowych nie chcesz wykonać równolegle do_something () i do_something_other (), proponuję zachować prostotę i iść w ten sposób.



1

W przypadku czystych obietnic może być nieco bardziej bałagan, ale jeśli używasz Odroczonych obietnic, nie jest tak źle:

Zainstalować:

npm install --save @bitbar/deferred-promise

Zmodyfikuj swój kod:

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);
});
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.