process.nextTick, setImmediate và setTimeout

process.nextTick, setImmediate và setTimeout

Tin ngắn hàng ngày dành cho bạn
  • R1 chưa qua, R2 đã tới 😅

    Mặc dù mới đang đồn thổi thôi cơ mà chắc chỉ có Deepseek mới gây tiếng vang tương tự như OpenAI hoặc Anthropic. Mà thứ người dùng quan tâm là chất lượng & giá cả 😄

    Deepseek R2 will be releasing soon

    » Xem thêm
  • Opensource này giúp bạn tạo nhanh một trang cá nhân giới thiệu về bản thân (giống như CV á): self.so

    » Xem thêm
  • Cuộc đua mô hình ngày càng khốc liệt khiến các công ty công nghệ không ai muốn bỏ lại phía sau. Llama 4 Scout, Llama 4 Maverick là hai mô hình mã nguồn mở mới nhất của Meta, được quảng cáo với hiệu năng vượt trội, thậm chí còn đánh bại các mô hình tiên tiến nhất như GPT-4.5, Claude Sonnet 3.7, hay Gemini 2.0 Pro... Nhưng bênh cạnh đó, Scout và Maverick đang vấp phải chỉ trích về việc gian lận. Liệu chúng ta có nên đặt lòng tin vào Meta thêm lần nữa 🤔

    Llama 4 Scandal: Meta’s release of Llama 4 overshadowed by cheating allegations on AI benchmark

    » 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

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên 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.

Bình luận (1)

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