Điều phối thực thi song song trong node.js


79

Mô hình lập trình hướng sự kiện của node.js làm cho việc điều phối luồng chương trình hơi phức tạp.

Thực hiện tuần tự đơn giản được chuyển thành các lệnh gọi lại lồng nhau, điều này đủ dễ dàng (mặc dù hơi phức tạp để viết ra).

Nhưng làm thế nào về thực hiện song song? Giả sử bạn có ba tác vụ A, B, C có thể chạy song song và khi chúng hoàn thành, bạn muốn gửi kết quả của chúng cho tác vụ D.

Với mô hình fork / join, điều này sẽ là

  • ngã ba A
  • ngã ba B
  • ngã ba C
  • tham gia A, B, C, chạy D

Làm cách nào để viết điều đó trong node.js? Có bất kỳ phương pháp hay nhất hoặc sách nấu ăn nào không? Tôi có phải thực hiện thủ công một giải pháp mỗi lần hay có thư viện nào đó có người trợ giúp cho việc này không?

Câu trả lời:


128

Không có gì thực sự song song trong node.js vì nó là một luồng. Tuy nhiên, nhiều sự kiện có thể được lên lịch và chạy theo một trình tự mà bạn không thể xác định trước. Và một số thứ như truy cập cơ sở dữ liệu thực sự "song song" trong đó bản thân các truy vấn cơ sở dữ liệu được chạy trong các luồng riêng biệt nhưng được tích hợp lại vào luồng sự kiện khi hoàn thành.

Vì vậy, làm cách nào để bạn lập lịch gọi lại trên nhiều trình xử lý sự kiện? Đây là một kỹ thuật phổ biến được sử dụng trong hoạt ảnh trong javascript phía trình duyệt: sử dụng một biến để theo dõi quá trình hoàn thành.

Điều này nghe có vẻ giống như một vụ hack và đúng như vậy, và nó có vẻ lộn xộn khi để lại một loạt các biến toàn cục xung quanh việc theo dõi và nó sẽ là một ngôn ngữ đơn giản hơn. Nhưng trong javascript chúng ta có thể sử dụng các bao đóng:

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var callback = function () {
    counter --;
    if (counter == 0) {
      shared_callback()
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](callback);
  }
}

// usage:
fork([A,B,C],D);

Trong ví dụ trên, chúng tôi giữ cho mã đơn giản bằng cách giả sử các hàm không đồng bộ và gọi lại không yêu cầu đối số. Tất nhiên, bạn có thể sửa đổi mã để chuyển các đối số cho các hàm không đồng bộ và để hàm gọi lại tích lũy kết quả và chuyển nó đến hàm shared_callback.


Câu trả lời bổ sung:

Trên thực tế, ngay cả như vậy, fork()hàm đó đã có thể chuyển các đối số cho các hàm không đồng bộ bằng cách sử dụng một bao đóng:

fork([
  function(callback){ A(1,2,callback) },
  function(callback){ B(1,callback) },
  function(callback){ C(1,2,callback) }
],D);

việc duy nhất cần làm là tích lũy kết quả từ A, B, C và chuyển chúng cho D.


Thậm chí nhiều câu trả lời bổ sung:

Tôi không thể cưỡng lại. Hãy nghĩ về điều này trong bữa sáng. Đây là cách triển khai fork()tích lũy kết quả (thường được truyền dưới dạng đối số cho hàm gọi lại):

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var all_results = [];
  function makeCallback (index) {
    return function () {
      counter --;
      var results = [];
      // we use the arguments object here because some callbacks 
      // in Node pass in multiple arguments as result.
      for (var i=0;i<arguments.length;i++) {
        results.push(arguments[i]);
      }
      all_results[index] = results;
      if (counter == 0) {
        shared_callback(all_results);
      }
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](makeCallback(i));
  }
}

Đó là đủ dễ dàng. Điều này tạo nên fork()mục đích khá chung và có thể được sử dụng để đồng bộ hóa nhiều sự kiện không đồng nhất.

Cách sử dụng ví dụ trong Node.js:

// Read 3 files in parallel and process them together:

function A (c){ fs.readFile('file1',c) };
function B (c){ fs.readFile('file2',c) };
function C (c){ fs.readFile('file3',c) };
function D (result) {
  file1data = result[0][1];
  file2data = result[1][1];
  file3data = result[2][1];

  // process the files together here
}

fork([A,B,C],D);

Cập nhật

Mã này được viết trước khi có các thư viện như async.js hoặc các thư viện dựa trên hứa hẹn khác nhau. Tôi muốn tin rằng async.js được lấy cảm hứng từ điều này nhưng tôi không có bất kỳ bằng chứng nào về điều đó. Dù sao .. nếu bạn đang nghĩ đến việc này ngày hôm nay, hãy xem async.js hoặc các lời hứa. Chỉ cần coi câu trả lời ở trên là một lời giải thích / minh họa hay về cách những thứ như async.parallel hoạt động.

Vì lợi ích hoàn chỉnh, sau đây là cách bạn thực hiện với async.parallel:

var async = require('async');

async.parallel([A,B,C],D);

Lưu ý rằng nó async.parallelhoạt động hoàn toàn giống như forkchức năng chúng tôi đã triển khai ở trên. Sự khác biệt chính là nó chuyển một lỗi làm đối số đầu tiên Dvà gọi lại làm đối số thứ hai theo quy ước node.js.

Sử dụng lời hứa, chúng tôi viết nó như sau:

// Assuming A, B & C return a promise instead of accepting a callback

Promise.all([A,B,C]).then(D);

12
"Không có gì thực sự song song trong node.js vì nó là một luồng đơn." Không đúng. Mọi thứ không sử dụng CPU (chẳng hạn như chờ I / O mạng) chạy song song.
Thilo

3
Đó là sự thật, đối với hầu hết các phần. Chờ đợi IO trong Node không chặn mã khác chạy, nhưng khi mã được chạy, nó là từng cái một. Việc thực thi song song thực sự duy nhất trong Node là từ các tiến trình con sinh sản, nhưng sau đó có thể nói về gần như bất kỳ môi trường nào.
MooGoo

6
@Thilo: Thông thường chúng ta gọi mã không sử dụng CPU là không chạy. Nếu bạn không chạy, bạn không thể "chạy" song song.
slbetman

4
@MooGoo: Hàm ý của điều này là với các sự kiện, bởi vì chúng tôi biết chúng chắc chắn không thể chạy song song, nên chúng tôi không phải lo lắng về các semaphores và mutexes trong khi với các luồng, chúng tôi phải khóa các tài nguyên được chia sẻ.
slbetman

2
Tôi có đúng khi nói rằng đây không phải là các hàm thực thi song song, nhưng chúng (tốt nhất) đang thực thi theo một trình tự không xác định với mã không tiến triển cho đến khi mỗi 'async_func' trả về?
Aaron Rustad

10

Tôi tin rằng bây giờ mô-đun "async" cung cấp chức năng song song này và gần giống với chức năng fork ở trên.


2
Điều này không chính xác, không đồng bộ chỉ giúp bạn tổ chức dòng mã của mình trong một quy trình duy nhất.
bwindels,

2
async.parallel không thực sự làm khoảng điều tương tự như trên forkchức năng
Dave Stibrany

nó không phải là một sự song song thực sự
rab

5

Các kỳ hạn mô-đun có một submodule gọi tham gia mà tôi đã có thể thích để sử dụng:

Nối các cuộc gọi không đồng bộ với nhau tương tự như cách pthread_joinhoạt động của các luồng.

Readme hiển thị một số ví dụ điển hình về việc sử dụng nó theo kiểu tự do hoặc sử dụng mô-đun con trong tương lai bằng cách sử dụng mẫu Promise. Ví dụ từ tài liệu:

var Join = require('join')
  , join = Join()
  , callbackA = join.add()
  , callbackB = join.add()
  , callbackC = join.add();

function abcComplete(aArgs, bArgs, cArgs) {
  console.log(aArgs[1] + bArgs[1] + cArgs[1]);
}

setTimeout(function () {
  callbackA(null, 'Hello');
}, 300);

setTimeout(function () {
  callbackB(null, 'World');
}, 500);

setTimeout(function () {
  callbackC(null, '!');
}, 400);

// this must be called after all 
join.when(abcComplete);

2

Có thể có một giải pháp đơn giản tại đây: http://howtonode.org/control-flow-part-ii cuộn đến Hành động song song. Một cách khác là để A, B và C đều chia sẻ cùng một hàm gọi lại, hãy để hàm đó có một bộ tăng toàn cục hoặc ít nhất là ngoài hàm, nếu cả ba đã gọi hàm gọi lại thì hãy để nó chạy D, tất nhiên bạn cũng sẽ phải lưu trữ kết quả của A, B và C ở đâu đó.




0

Ngoài các hứa hẹn phổ biến và thư viện không đồng bộ, còn có cách tốt thứ 3 - sử dụng "hệ thống dây":

var l = new Wire();

funcA(l.branch('post'));
funcB(l.branch('comments'));
funcC(l.branch('links'));

l.success(function(results) {
   // result will be object with results:
   // { post: ..., comments: ..., links: ...}
});

https://github.com/garmoshka-mo/mo-wire

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 cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.