Идиоматический способ ожидания нескольких обратных вызовов в Node.js


99

Предположим, вам нужно выполнить некоторые операции, которые зависят от какого-то временного файла. Поскольку мы говорим здесь о Node, эти операции, очевидно, асинхронны. Каков идиоматический способ дождаться завершения всех операций, чтобы узнать, когда можно удалить временный файл?

Вот код, показывающий, что я хочу сделать:

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

Но если я напишу это так, третий вызов может быть выполнен до того, как первые два получат возможность использовать файл. Мне нужен какой-то способ гарантировать, что первые два вызова уже завершились (вызвали их обратные вызовы), прежде чем двигаться дальше, не вкладывая вызовы (и делая их синхронными на практике).

Я подумал об использовании эмиттеров событий для обратных вызовов и регистрации счетчика в качестве получателя. Счетчик получал завершенные события и подсчитывал, сколько операций еще не завершено. Когда последний закончит, он удалит файл. Но есть риск состояния гонки, и я не уверен, что это обычно так.

Как люди, работающие с Node, решают такие проблемы?


Спасибо за этот вопрос, у меня тоже есть похожая проблема.
— Кришна Шетти

Ответы:


94

Обновить:

А теперь посоветовал бы взглянуть на:

  • Обещания

    Объект Promise используется для отложенных и асинхронных вычислений. Promise представляет собой операцию, которая еще не завершена, но ожидается в будущем.

    Популярная библиотека обещаний - bluebird . А посоветовал бы посмотреть, почему обещает .

    Вы должны использовать обещания, чтобы это исправить:

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

    В это:

    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");
    });
  • генераторы: например, через co .

    Эффективность потока управления на основе генератора для nodejs и браузера с использованием обещаний, позволяющая писать неблокирующий код в приятной манере.

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

Если я правильно понимаю, думаю, вам стоит взглянуть на очень хорошую асинхронную библиотеку. Особенно стоит взглянуть на сериал . Просто копия фрагментов со страницы 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}
});

В качестве плюса эту библиотеку также можно запускать в браузере.


21
На самом деле я использовал async.parallel, поскольку операции независимы, и я не хотел заставлять их ждать выполнения предыдущих.
— Thiago Arrais 03

22

Самый простой способ увеличить целочисленный счетчик, когда вы запускаете асинхронную операцию, а затем в обратном вызове уменьшить счетчик. В зависимости от сложности обратный вызов может проверить счетчик на ноль, а затем удалить файл.

Немного сложнее было бы вести список объектов, и каждый объект имел бы любые атрибуты, необходимые для идентификации операции (это может быть даже вызов функции), а также код состояния. Обратные вызовы установят код состояния как завершенный.

Тогда у вас будет цикл, который ожидает (использует process.nextTick) и проверяет, все ли задачи выполнены. Преимущество этого метода над счетчиком заключается в том, что, если все невыполненные задачи могут быть выполнены до того, как все задачи будут выпущены, метод счетчика приведет к преждевременному удалению файла.


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

Не существует "собственного" решения, но для узла существует миллион библиотек управления потоком . Вам может понравиться 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);
  }
)

Или, как предложил Майкл, счетчики могут быть более простым решением. Взгляните на этот макет семафора . Вы бы использовали это так:

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

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

6

Я хотел бы предложить другое решение, которое использует скорость и эффективность парадигмы программирования в самом ядре событий Node :.

Все, что вы можете делать с промисами или модулями, предназначенными для управления потоком, например async, может быть выполнено с помощью событий и простого конечного автомата, который, я считаю, предлагает методологию, которая, возможно, легче понять, чем другие варианты.

Например, предположим, что вы хотите суммировать длину нескольких файлов параллельно:

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

Что выведет:

start: начало обработки 4-х дорожек
forEachPath: запуск 4 цепочек процессов
  readPath: file1
  readPath: file2
  processContent: file1
  readPath: file3
  processContent: file2
  processContent: file3
  readPath: file4
  processContent: file4
Всего 4 файла 4021

Обратите внимание, что порядок задач цепочки процессов зависит от загрузки системы.

Вы можете представить себе выполнение программы как:

начало -> forEachPath - + -> readPath 1 -> processContent 1 - + -> готово
                      + -> readFile 2 -> processContent 2 - +
                      + -> readFile 3 -> processContent 3 - +
                      + -> readFile 4 -> processContent 4 - +

Для повторного использования было бы тривиально создать модуль для поддержки различных шаблонов управления потоком, то есть последовательного, параллельного, пакетного, пока, пока и т. Д.


2

Самое простое решение - запустить do_something * и отсоединить последовательно следующим образом:

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

Если по соображениям производительности вы не хотите выполнять do_something () и do_something_other () параллельно, я предлагаю не усложнять задачу и идти этим путем.



1

С чистыми обещаниями это может быть немного более беспорядочно, но если вы используете отложенные обещания, это не так уж плохо:

Установить:

npm install --save @bitbar/deferred-promise

Измените свой код:

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);
});
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.