Cách đơn giản để đợi nhiều lệnh gọi lại trong Node.js


99

Giả sử bạn cần thực hiện một số thao tác phụ thuộc vào một số tệp tạm thời. Vì chúng ta đang nói về Node ở đây, các hoạt động đó rõ ràng là không đồng bộ. Cách thành ngữ là đợi cho tất cả các thao tác kết thúc để biết khi nào có thể xóa tệp tạm thời?

Đây là một số mã hiển thị những gì tôi muốn làm:

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

Nhưng nếu tôi viết nó theo cách này, cuộc gọi thứ ba có thể được thực hiện trước khi hai lệnh đầu tiên có cơ hội sử dụng tệp. Tôi cần một số cách để đảm bảo rằng hai cuộc gọi đầu tiên đã kết thúc (đã gọi các lệnh gọi lại của chúng) trước khi tiếp tục mà không lồng các cuộc gọi (và làm cho chúng đồng bộ trong thực tế).

Tôi đã nghĩ đến việc sử dụng bộ phát sự kiện trên các lệnh gọi lại và đăng ký bộ đếm làm bộ thu. Bộ đếm sẽ nhận các sự kiện đã hoàn thành và đếm có bao nhiêu hoạt động vẫn đang chờ xử lý. Khi kết thúc cuối cùng, nó sẽ xóa tệp. Nhưng có nguy cơ xảy ra tình trạng cuộc đua và tôi không chắc đây thường là cách những thứ này được thực hiện.

Làm thế nào để người Node giải quyết loại vấn đề này?


Cảm ơn cho câu hỏi này, tôi cũng có vấn đề tương tự.
— Krishna Shetty

Câu trả lời:


94

Cập nhật:

Bây giờ tôi khuyên bạn nên xem qua:

  • Lời hứa

    Đối tượng Promise được sử dụng để tính toán chậm và không đồng bộ. Lời hứa đại diện cho một hoạt động chưa hoàn thành, nhưng được mong đợi trong tương lai.

    Một thư viện hứa hẹn phổ biến là bluebird . A sẽ tư vấn để có một cái nhìn tại sao lời hứa .

    Bạn nên sử dụng các lời hứa để biến điều này:

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

    Vào cái này:

    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");
    });
  • máy phát điện: Ví dụ thông qua co .

    Tính tốt của luồng điều khiển dựa trên trình tạo cho nodejs và trình duyệt, sử dụng các hứa hẹn, cho phép bạn viết mã không chặn theo một cách tốt đẹp.

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

Nếu tôi hiểu đúng, tôi nghĩ bạn nên xem thư viện async rất tốt . Bạn đặc biệt nên xem qua loạt bài này . Chỉ là một bản sao từ các đoạn trích từ trang 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}
});

Ngoài ra, thư viện này cũng có thể chạy trong trình duyệt.


21
Tôi thực sự đã kết thúc bằng cách sử dụng async.parallel, vì các hoạt động là độc lập và tôi không muốn bắt chúng chờ đợi ở những cái trước.
— Thiago Arrais

22

Cách đơn giản nhất là tăng bộ đếm số nguyên khi bạn bắt đầu hoạt động không đồng bộ và sau đó, trong lệnh gọi lại, giảm bộ đếm. Tùy thuộc vào độ phức tạp, lệnh gọi lại có thể kiểm tra bộ đếm về 0 và sau đó xóa tệp.

Phức tạp hơn một chút sẽ là duy trì danh sách các đối tượng và mỗi đối tượng sẽ có bất kỳ thuộc tính nào mà bạn cần xác định hoạt động (thậm chí nó có thể là lệnh gọi hàm) cũng như mã trạng thái. Các lệnh gọi lại sẽ đặt mã trạng thái thành hoàn thành.

Sau đó, bạn sẽ có một vòng lặp chờ (sử dụng process.nextTick) và kiểm tra xem tất cả các tác vụ đã hoàn thành chưa. Ưu điểm của phương pháp này so với bộ đếm, là nếu có thể hoàn thành tất cả các tác vụ chưa hoàn thành trước khi tất cả các tác vụ được phát hành, kỹ thuật bộ đếm sẽ khiến bạn xóa tệp sớm.


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

Không có giải pháp "nguyên gốc", nhưng có hàng triệu thư viện điều khiển luồng cho nút. Bạn có thể thích Bước:

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

Hoặc, như Michael gợi ý, quầy có thể là một giải pháp đơn giản hơn. Hãy xem mô hình semaphore này . Bạn sẽ sử dụng nó như thế này:

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

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

6

Tôi muốn đưa ra một giải pháp khác sử dụng tốc độ và hiệu quả của mô hình lập trình ở cốt lõi của Node: sự kiện.

Mọi thứ bạn có thể làm với Promises hoặc mô-đun được thiết kế để quản lý điều khiển luồng, chẳng hạn như async, có thể được thực hiện bằng cách sử dụng các sự kiện và một máy trạng thái đơn giản, mà tôi tin rằng cung cấp một phương pháp luận, có lẽ, dễ hiểu hơn các tùy chọn khác.

Ví dụ: giả sử bạn muốn tính tổng độ dài của nhiều tệp song song:

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

Cái nào sẽ xuất ra:

start: bắt đầu xử lý 4 đường dẫn
forEachPath: bắt đầu 4 chuỗi quy trình
  readPath: file1
  readPath: file2
  processContent: file1
  readPath: file3
  processContent: file2
  processContent: file3
  readPath: file4
  processContent: file4
Tổng 4 tệp là 4021

Lưu ý rằng thứ tự của các nhiệm vụ chuỗi quy trình phụ thuộc vào tải hệ thống.

Bạn có thể hình dung luồng chương trình như sau:

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

Để sử dụng lại, sẽ rất đơn giản nếu tạo một mô-đun để hỗ trợ các mẫu điều khiển luồng khác nhau, tức là loạt, song song, hàng loạt, trong khi, cho đến khi, v.v.


2

Giải pháp đơn giản nhất là chạy do_something * và hủy liên kết theo trình tự như sau:

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

Trừ khi, vì lý do hiệu suất, bạn muốn thực thi do_something () và do_something_other () song song, tôi khuyên bạn nên giữ nó đơn giản và thực hiện theo cách này.



1

Với Lời hứa thuần túy, nó có thể lộn xộn hơn một chút, nhưng nếu bạn sử dụng Lời hứa trì hoãn thì nó không quá tệ:

Tải về:

npm install --save @bitbar/deferred-promise

Sửa đổi mã của bạn:

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);
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.