Kiến trúc Node.js - process.nextTick, setImmediate và setTimeout

Kiến trúc Node.js - process.nextTick, setImmediate và setTimeout

Tin ngắn hàng ngày dành cho bạn
  • Mấy hôm nay mình đang bận lên bản cập nhật cho OpenNotas nên không đăng tin mới 😅

    » Xem thêm
  • Vậy là cuối cùng Anthropic đã ra trang web Anthropic Academy. Dạy tất tần tật những thứ liên quan đến Claude luôn.

    » Xem thêm
  • Rust sinh ra để tái định nghĩa nhiều thứ. Trong đó chắc phải kể đến JavaScript. Từ đầu năm đến giờ các công cụ làm từ Rust mà để cho JavaScript dùng đếm sương sương cũng vài ba cái rồi. Mới đây nhất là Oxc.

    Oxc là một công cụ phân tích cú pháp (parser), kiểm tra lỗi (lint), định dạng (formatter), chuyển đổi (transformer), minifier... tất cả đều được viết bằng Rust, trong một công cụ duy nhất.

    Mặc dù vẫn đang trong quá trình xây dựng nhưng thử nhìn điểm hiệu năng của nó so với swc hoặc eslint mà xem 🫣

    » Xem thêm

Vấn đề

Ở bài viết trước, chúng ta đã biết 6 pha xử lý trong một vòng lặp sự kiện, về chức năng và các kiểu hàm callback nào được xử lý ở đâu. Ngoài ra còn có một pha đặc biệt gọi là process.nextTick tuy không thuộc Event loop nhưng lại có mức độ ưu tiên cao nhất.

Khái niệm tưởng chừng dễ nhưng lại là gây khó dễ cho nhiều người, kể cả lập trình viên lâu năm. Vậy khi đặt process.nextTick, setImmediatesetTimeout lại cùng với nhau. Theo bạn callback trong hàm nào được thực hiện trước?

Nhắc lại Event loop

Trước tiên hãy quay lại mô hình 6 pha của vòng lặp sự kiện.

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Mỗi pha có nhiệm vụ xử lý các sự kiện riêng biệt. Chúng ta không thể đẩy ồ ạt sự kiện vào 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 pha giúp đảm bảo đ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 setTimeout được xử lý trong "timers" nằm ở trên cùng của Event loop. Trong khi setImmediate được xử lý trong "check". Nếu 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?

setTimeout & setImmediate

Trong một chương trình đồng bộ, tức là không gọi hàm bất đồng bộ nào cả. 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 lên lịch trong pha "check". Vậy theo lý thuyết, callback của setTimeout luôn được thực hiện trước setImmediate.

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

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

Khi chạy mã trên bạn sẽ thấy 2 chuỗi "setTimeout" và "setImmediate" xuất hiện không theo thứ tự nào cả, tức là có lúc thì "setTimeout" in ra trước, lúc thì lại in ra sau. Lý giải cho điều này là khi mới bắt đầu chương trình, Node.js 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 chương trình có gọi hàm bất đồng bộ 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 lặp nữa để đến pha "timers".

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

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

Vì vậy, chúng ta nên sử dụng setImmediate để chạy 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 nhật ký truy vấn (request logs). Ví dụ như trong 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.

Điều đó chưa chắc đã đú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 giúp đẩy phần công việc ghi nhật ký vào pha "check", nhường thời gian cho pha "poll" - vốn được dùng để xử lý callback của các chức năng chính, tăng hiệu suất máy chủ. Luôn luôn nhớ rằng đẩy các công việc bớt quan trọng hơn xuống pha "check", nhường thời gian xử lý cho "poll".

Ngoài ra setImmediate còn được sử dụng để giảm tải luồng chính. Để biết thêm thông tin chi tiết, bạn đọc xem 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). Trong bài viết đề cập đến phương pháp "Partitioning" dựa trên setImmediate để ngăn chặn vòng lặp sự kiện khi muốn xử lý một lượng lớn dữ liệu.

process.nextTick

Nhìn lại 6 pha của Event loop bạn không thấy bất kỳ pha nào xử lý callback của process.nextTick. Đó là do process.nextTick là một pha đặc biệt, callback của nó luôn luôn được xử lý trên đầu mỗi pha của vòng lặp sự kiện.

Ví dụ, Event loop xử lý hết callback của "timers", chuẩn bị chuyển sang "pending callbacks" thì hẵng khoan đã, nó xử lý callback trong process.nextTick, sau khi xong hết thì mới chuyển sang "pending callbacks"... và cứ như thế ở các pha tiếp theo. Như vậy tức là process.nextTick có độ ưu tiên rất cao trong Event loop.

Nên ứ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.

Kết luận

Qua bài viết này, chúng ta đã đi sâu vào cách hoạt động của process.nextTick, setImmediatesetTimeout trong Node.js, từ đó hiểu rõ vai trò của chúng trong Event loop. process.nextTick nổi bật với mức ưu tiên cao nhất, được thực thi ngay trước khi chuyển sang các pha khác của Event loop, là lựa chọn lý tưởng cho những công việc cần thực hiện ngay lập tức. Trong khi đó, setImmediate được ưu tiên sử dụng để xử lý các tác vụ ít quan trọng hơn sau khi hoàn thành I/O, giúp giảm tải cho vòng lặp sự kiện và tối ưu hiệu suất hệ thống. Còn setTimeout thường được dùng để lên lịch thực thi ở các vòng lặp sau, nhưng lại dễ bị ảnh hưởng bởi độ trễ ban đầu của Node.js.

Hiểu rõ sự khác biệt này không chỉ giúp lập trình viên sử dụng các hàm callback một cách hiệu quả hơn, mà còn tối ưu hóa hiệu suất ứng dụng, đặc biệt trong các hệ thống đa nhiệm và yêu cầu xử lý đồng thời cao. Tùy thuộc vào kịch bản, việc chọn đúng công cụ giữa process.nextTick, setImmediatesetTimeout có thể tạo ra sự khác biệt lớn trong cách ứng dụng của bạn hoạt động và phản hồi. Hãy sử dụng chúng một cách khôn ngoan để đảm bảo rằng các tác vụ được xử lý theo đúng thứ tự ưu tiên và phù hợp với yêu cầu của hệ thống.

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.

Bình luận (1)

Nội dung bình luận...
Avatar
Trịnh Cường7 tháng trước

quá hay bạn ơi

Trả lời
Avatar
Xuân Hoài Tống7 tháng trước

Cảm ơn bạn Cường nha 🙏