“Bí ẩn” vòng lặp trong Nodejs

Tôi đã và đang dành nhiều thời gian để viết code chạy trên môi trường NodeJS. Tuy không phải một người cuồng ngôn ngữ Javascript, nhưng tôi cũng thích một phần của ngôn ngữ này.

Cách đây vài năm, vào những ngày đầu tôi làm việc ở vị trí lập trình viên Nodejs, công việc làm tôi hiểu sâu hơn về ngôn ngữ Nodejs và giờ tôi làm việc với nó hằng ngày.

Bài viết này sẽ là một chia sẻ ngắn, câu chuyện của cá nhân, kinh nghiệm cực kỳ đáng nhớ khi cố làm một cái gì đó đơn giản hơn.

Câu chuyện của nhiều năm trước

Đây là cách mà nó bắt đầu: Tôi đã viết một tool để lấy dữ liệu từ một trang web thương mại điện tử (tạm gọi là XYZ). Ý tưởng của tôi rất đơn giản – Tôi muốn có danh sách tất cả các sản phẩm và thông tin của chúng ở dưới dạng JSON, để sau này có thể sử dụng dữ liệu đó một cách dễ dàng.

Sau khi xem xét cấu trúc của trang web, tôi nhận thấy mình có thể lấy thông tin của tất cả các sản phẩm bằng cách duyệt qua tất cả các số nguyên – các số có thể là ID của sản phẩm. Khá may mắn vì XYZ cho phép truy cập các sản phẩm qua ID là các số nguyên luôn tăng:

  • http://xy.z/products/1
  • http://xy.z/products/2
  • http://xy.z/products/3
  • http://xy.z/products/4

Nguyên tắc khá đơn giản: Nếu chúng ta gặp http status là 404 thì bỏ qua, nếu là status là 200 thì chúng ta sẽ có được thông tin của sản phẩm!

Chính vì vậy, tôi đã viết phiên bản đầu tiên của chương trình giống như thế này:

var request = require('request');

for (var i = 0; i < 10000000; i++) {
  request('http:/xy.z/' + i, ...);
}

Mọi thứ khá cơ bản:

  • Lặp qua tất cả các số có thể là ID? – Đã xong
  • Tạo ra các http request để lấy thông tin sản phẩm? Đã xong

Điều làm tôi mất tinh thần đã xảy ra, sau khi chạy chương trình được 1, 2 phút, chương trình nhỏ xíu này của tôi đã chiếm hết toàn bộ RAM của chiếc laptop và tôi phải khởi động lại chiếc laptop!!! Nhưng tại sao???

Trước đó tôi đã biết Nodejs sẽ block khi chạy qua những blocking code (ví dụ: Những phép tính toán nặng…). Nhưng ở đây, rõ ràng là tôi sử dụng một tác vụ bất đồng bộ (request http) và nó phải hoạt động bình thường mới đúng (vì tác vụ bất đồng bộ sẽ không lock chương trình mà :-?).

Tôi đã sai! Chắc chắn rồi.

Nhưng, hơi có chút bối rối với chuyện đang xảy ra. Tôi quyết định tìm hiểu sâu thêm một chút vấn đề này. Tôi sẽ thu hẹp vấn đề mà mình đang gặp phải, làm cho nó đơn giản hơn một chút:

for (var i = 0; i < 10000000; i++) {
  console.log('ping:', i);
}

Hix, xảy ra cùng một vấn đề – nó vẫn xảy ra, chương trình chạy trong vài phút, sau đó nó lại chiếm toàn bộ RAM của chiếc máy tính, và sự cố tương tự như trường hợp ở trên. 😐

Tôi đã “Googling” để tìm kiếm một giải pháp, hay đơn giản là một lời giải thích cho vấn đề này vì tôi nghĩ chắc chắn đây phải là một vấn đề phổ biến?

Thật không may, tôi không tìm được thứ mình muốn. Các cuộc thảo luận trên Stackoverflow cũng chỉ đề xuất các giải pháp không dùng vòng lặp(không phải tùy chọn trong trường hợp của tôi).

Tiếp theo, tôi chuyển sang async – Một thư viện để làm việc bất đồng bộ khá phổ biến. Sau khi đọc lướt qua một lượt tài liệu hướng dẫn sử dụng, tôi nhận ra có một thứ dường như hoàn hảo trong trường hợp này, phương thức forever của thư viện async. Tôi đã thử vơi đoạn code như sau:

var async = require('async');

var i = 0;
async.forever(
  function(next) {
    console.log('ping:', i);
    i++;
    next(i === 1000000 ? i : null);
  },
  function(err) {
    console.log('All done!');
  }
);

Nhưng một lần nữa, vấn đề tương tự. Sau khoảng vài ngàn vòng lặp: Crash

Sau khi thử nhiều cách khác nữa, tôi vẫn không giải quyết được vấn đề, mọi thứ gần như đi vào bế tắc. Thật may, đồng nghiệp của tôi – một coworker, đã đưa tôi một giải pháp tuyệt vời:

var MagicLoop = function () {
  this.index = -1;
};

MagicLoop.prototype.getIndex = function getIndex() {
  this.index++;
  return this.index;
};

MagicLoop.prototype.isDone = function isDone() {
  return this.index > 10000000;
};

var list = new MagicLoop();

function iterator() {
  var i = list.getIndex();
  console.log(i);
  if (list.isDone()) {
    clearInterval(interval);
  }
}

var interval = setInterval(iterator, 1);

Tuyệt vời! Thậm chí tôi đã nghĩ tới sẽ sử dụng setInterval nhưng lại bỏ qua nó.

Dù sao đi nữa, sau nhiều cuộc thảo luận, cả hai chúng tôi thống nhất rằng sử dụng hàm setInterval về cơ bản là cách duy nhất để giải quyết vấn đề này.

Kết luận

Việc thực hiện các tác vụ không đồng bộ với các vòng lặp trong Nodejs không hề đơn giản như tôi nghĩ. Tôi đã thấy khá kỳ lạ vì những thứ đơn giản như một vòng lặp có thể làm hỏng một chương trình.

Một bài học rất thú vị!

Từ khóa: , , ,