process.nextTick, setImmediate và setTimeout

process.nextTick, setImmediate và setTimeout

Những mẩu tin ngắn hàng ngày dành cho bạn
  • smee.io là một cách đơn giản để tạo một địa chỉ webhook và ánh xạ nó về địa chỉ localhost trong máy tính của bạn.

    $ npm install --global smee-client $ smee -u https://smee.io/eu4UoW8vrKSZtTB

    » Xem thêm
  • javascript.tm được Ryan Dahl - cha đẻ của Node.js tạo ra với mục tiêu trình bày và kêu gọi cộng đồng ủng hộ việc Oracle "giải phóng" cái tên JavaScript, trả lại nó cho cộng đồng.

    Anh cho rằng Oracle sau nhiều thương vụ mua bán đã gián tiếp sở hữu cái tên JavaScript, nhưng họ lại không sử dụng cái tên này. Một điều nữa là JavaScript đã trở thành tên gọi chung, cộng đồng sử dụng rất nhiều mà ít ai biết rằng nó thuộc về Oracle.

    Việc Oracle nắm giữ thương hiệu này gây ra sự nhầm lẫn không cần thiết. Anh kêu gọi Oracle từ bỏ quyền sở hữu, trả nó về với cộng đồng. Nếu được như vậy, nhiều hiểu lầm được xoá bỏ và mọi người sẽ có nhiều quyền hạn với cái tên này. Ví dụ, chúng ta sẽ có một "Hội nghị về JavaScript" trong tương lai mà không phải lo lắng về vấn đề pháp lý.

    Thông tin mới nhất, Vào ngày 22 tháng 11 năm 2024: Anh đã nộp đơn lên USPTO. Chúng ta cùng chờ xem kết quả ra sao nhé! Ngoài ra bạn cũng có thể bày tỏ sự ủng hộ bằng cách truy cập vào địa chỉ ở trên và tiến hành "ký" vào lá đơn 😇

    » Xem thêm
  • Nhớ mấy ngày đầu đi phỏng vấn, có một câu hỏi là JavaScript có phải là Java không? Thì mình trả lời ngay là có 😆. Buồn cười thật, vì đúng ra JavaScript và Java là hai ngôn ngữ khác nhau hoàn toàn.

    Sự nhầm lẫn này mà mới đây đã dẫn đến vụ kiện đòi Oracle trả lại cái tên JavaScript cho cộng đồng. Oracle? Đúng vậy, cái tên liên quan rất mật thiết đến Java, nhưng tại sao lại kiện họ với JavaScript cơ chứ?

    Hic, hôm nay làm việc mệt mỏi quá nên xin phép ngày mai mình kể chi tiết hơn nha vì câu chuyện khá dài đấy 😅

    » Xem thêm

Vấn đề

Bất kỳ ai khi tìm hiểu sâu vào Node.js có thể đã thấy các bài viết phân biệt sự khác nhau giữa setTimeout, setImmediateprocess.nextTick. Tôi cũng không phải là ngoại lệ! Thời gian đầu, tôi luôn cố gắng hiểu được cách sử dụng chúng bằng cách đọc các bài viết và cả tài liệu của Node. Nhưng hầu như chúng đều mang nặng tính lý thuyết. Nghĩ cũng phải vì để thực sự hiểu được sự khác nhau cũng như cách dùng, cần phải hiểu được cách hoạt động của vòng lặp sự kiện - event loop.

Trên thực tế, tôi rất ít khi quan tâm đến sự khác nhau giữa chúng. Tức là chương trình vẫn chạy được kể cả khi bạn biết hoặc không biết, vì chưa chắc bạn đã cần phải dùng đến. Còn nếu muốn chương trình chạy tốt hơn nữa thì chắc chắn cần phải học cách sử dụng.

Trước khi đi sâu vào bài viết, tôi khuyên bạn nên dành thời gian để đọc lại khái niệm về event loop, về cách nó hoạt động. Một số bài viết đã từ rất lâu trước đó của tôi ở đây: Kiến trúc Node.js - Event Loop, Tìm hiểu về vòng lặp sự kiện (Event Loop) trong Node.js. Còn tốt hơn nữa là bạn nên đọc tài liệu quý báu từ trang của của Node: The Node.js Event Loop. Thậm chí bạn cũng có thể tìm được bài viết về process.nextTick và setImmediate ở đây, nhưng nếu muốn khám phá chúng dưới cái nhìn khách quan của tôi, thì xin hãy đọc tiếp bài viết này!

Event loop

Event loop là trái tim của Node.js, một vòng lặp vô hạn để đẩy các hàm callback bất đồng bộ ngược lại callstack và xử lý.

Hãy nhớ lại 6 pha (pharse) của vòng lặp sự kiện. Một chu trình của vòng lặp phải đi qua 6 pha trước khi bắt đầu một cái mới. 6 pha đó như sau.

Timers phase: Xử lý các callback được lên lịch bằng setTimeoutsetInterval.

Pending callbacks phase: Xử lý các callback bị hoãn lại từ giai đoạn I/O trước đó.

Idle, prepare phase: Dành riêng cho các hoạt động nội bộ của Node.js.

Poll phase: Xử lý các tác vụ I/O mới, cũng như kiểm tra các sự kiện đã hoàn thành. Nếu không có sự kiện nào, nó sẽ chờ cho đến khi có một nhiệm vụ mới.

Check phase: Xử lý các callback từ setImmediate.

Close callbacks phase: Xử lý các sự kiện đóng của các tài nguyên (như socket.on('close', ...)).

Mỗi pha có nhiệm vụ xử lý các sự kiện riêng biệt. Sở dĩ Node.js phải chia ra các pha như vậy vì tính đơn luồng của nó. Chúng ta không thể đẩy vào ồ ạt sự kiện rồi mong chờ event loop xử lý chúng một cách lần lượt, mà có những sự kiện cần được xử lý trước các sự kiện khác. Việc chia ra các pha sẽ đảm bảo được điều đó!

Mỗi một chu trình lặp, Node.js đi qua từng pha và xử lý sự kiện trong đó trước khi sang pha khác. Ở đây chúng ta thấy setTimeout được xử lý trong Timers phase, cũng là pha đầu tiên của event loop. Khoan đã! setImmediate được xử lý trong Check phase, sau Timers phase, vậy thì chẳng phải setTimeout với thời gian chờ bằng 0 luôn thực hiện trước setImmediate? Vậy cái tên "immediate" có còn có ý nghĩa?

Chà, bạn đã bắt đầu bước vào sự phức tạp của Node. Còn câu trả lời cho câu hỏi trên là chưa hẳn. Tức là tuỳ vào trường hợp sử dụng mà cái nào được thực hiện trước cái nào.

setTimeout & setImmediate

Trong trường hợp không có tác vụ I/O nào được xử lý, tức là một chương trình hoàn toàn đồng bộ. Nếu sử dụng setTimeout(fn, 0), hàm callback sẽ được lên lịch trong pha "timers". Nếu sử dụng setImmediate(), hàm callback sẽ được thực thi trong pha "check". Vậy theo lý thuyết, callback của setTimeout được thực hiện trước setImmediate.

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

Nhưng thực tế, kết quả là setTimeoutsetImmediate sẽ "tranh nhau" thực hiện tuỳ vào Node đang ở pha nào. Tức là bạn sẽ thấy 2 chuỗi "setTimeout", "setImmediate" xuất hiện ngẫu nhiên trong các lần chạy khác nhau. Để lý giải là do khi mới bắt đầu chương trình, Node có độ trễ nhất định trước khi đi vào ổn định, tức là đôi khi nó ở pha "timers", đôi khi nó ở pha "check".

Trong trường hợp có I/O, thứ tự thực thi sẽ khác, luôn luôn là setImmediate trước tiên vì setImmediate được thực thi ngay sau khi tác vụ I/O hoàn thành, vì nó nằm trong pha "check" của event loop, vốn xảy ra ngay sau pha "poll" (giai đoạn xử lý các tác vụ I/O).

setTimeout(fn, 0) chỉ được thực thi sau khi event loop đã quay lại pha "timers", nghĩa là nó sẽ phải đợi một vòng event loop nữa để đến pha "timers".

fs.readFile("README.md", () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('setImmediate');
  });
});

Vậy chúng ta nên sử dụng setImmediate để chạy ngay một hàm callback ngay sau khi tác vụ I/O được hoàn thành.

setImmediate tỏ ra hữu ích khi được dùng để ghi logs request vào tệp. Hãy nhìn vào ví dụ về một máy chủ HTTP dưới đây:

res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World\n");

// ghi log vào file (công việc ít quan trọng hơn)
setImmediate(() => {
  fs.appendFile("server.log", `Request: ${req.url}\n`, (err) => {
    if (err) throw err;
    console.log("Logged request to server.log");
  });
});

Nhiều người nghĩ rằng không cần dùng đến setImmediate ở đây vì fs.appendFile là một hàm bất đồng bộ và nó không gây ảnh hưởng gì đến tốc độ phản hồi của phiên hiện tại. Do đó có thể loại bỏ setImmediate để tránh mã thừa.

Quan điểm đó chính xác nếu chỉ có một mình bạn dùng. Thực tế máy chủ luôn luôn nhận được rất nhiều truy vấn đến cùng một lúc. Việc sử dụng setImmediate sẽ đẩy công việc ghi logs vào pha Check, nhường thời gian cho pha Poll - vốn đang được dùng để xử lý callback của các chức năng chính. Làm như vậy sẽ tăng hiệu năng đáng kể cho trương trình, đẩy các công việc bớt quan trọng hơn xuống pha Check, sau pha Poll.

setImmediate còn được xử dụng để giảm tải luồng chính. Trong bài viết Hai kỹ thuật nhằm ngăn vòng lặp sự kiện (Event Loop) bị chặn khi xử lý tác vụ nặng (CPU-intensive task) trước đó, tôi đã đề cập đến phương pháp "Partitioning" dựa trên setImmediate để ngưng chặn vòng lặp sự kiện khi xử lý một lượng lớn dữ liệu.

process.nextTick

Hãy nhìn lại 6 pha, bạn sẽ không thấy bất kỳ pha nào xử lý callback của process.nextTick. Tại sao? Đó là do process.nextTick là một hàm đặc biệt, callback của nó luôn luôn được xử lý vào đầu mỗi pha của vòng lặp sự kiện.

Lấy ví dụ, event loop xử lý hết callback của pha "timers" -> xử lý callback trong process.nextTick -> xử lý callback của pha "pending callbacks" -> xử lý callback trong process.nextTick... và cứ như thế. Như vậy tức là process.nextTick có độ ưu tiên rất cao trong event loop.

Thế thì ứng dụng process.nextTick như thế nào? Câu trả lời rất rõ ràng, bất cứ khi nào muốn thực hiện công việc có mức ưu tiên cao nhất, trước khi các pha được xử lý để đảm bảo công việc được thực hiện ngay lập tức. Ví dụ process.nextTick cũng thường được áp dụng trong kỹ thuật "Partitioning" tương tự như setImmediate, chỉ có điều mỗi vòng lặp chỉ có 1 lần xử lý setImmediate, trong khi với process.nextTick thì con số lên đến 6 lần.

Vì lẽ đó trong thư viện bcrypt được dùng để tính toán chuỗi mã hoá - một tác vụ rất nặng về tính toán và có nguy cơ chặn event loop đã áp dụng triệt để process.nextTick để ngăn ngừa hành vi này.

Ngoài ra nó cũng thường được dùng để giảm tải cho luồng chính trong lần khởi động đầu tiên nhưng vẫn muốn các cấu hình trong constructor được tải ngay sau đó:

class MyClass {
  constructor() {
    process.nextTick(() => {
      this.loadConfiguration();
    });
  }

  loadConfiguration() {
    // load config here!
  }
}

const myClass = new MyClass();
Cao cấp
Hello

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (1)

Nội dung bình luận...
Avatar
Trịnh Cường2 tháng trước
quá hay bạn ơi
Trả lời
Avatar
Xuân Hoài Tống1 tháng trước
Cảm ơn bạn Cường nha 🙏