process.nextTick, setImmediate và setTimeout

process.nextTick, setImmediate và setTimeout

Threads
  • Ơ buồn cười thật. Ai cũng biết GIF là định dạng ảnh động thường được dùng thay cho video clip để hiển thị các nội dung ngắn trên nền tảng web. GIF tiện hơn các nội dung dạng video là vì nó được hiển thị như một bức ảnh và được hỗ trợ rộng rãi. Cơ mà GIF có dung lượng nặng quá.

    Nói thật nhiều lúc mình có vài cái hành động muốn hiển thị lên web cho mọi người xem, cơ mà định dạng GIF nó nặng với cả cũng không biết cách tối ưu cho nhẹ xuống. Hôm nay lên mạng tìm hiểu xem định dạng nào có khả năng thay thế GIF trong tương lai thì mọi người biết đó là gì không? Là WEBP (webp)!!! Đúng vậy, là định dạng ảnh mà mình đang dùng trên blog lâu nay luôn á, mà giờ mới biết là nó hiển thị được cả ảnh động nữa, hơi quê 😆

    Kết hợp với ffmpeg nữa là chuyển được tất tần tật video clip thành webp được ngay. Để vài nửa ngồi chế lại cái cli một tí là dùng ngon luôn mọi người ạ 🤪

    » Xem thêm
  • Cảm giác như Github Copilot đang cố gắng mở rộng thị trường cho anh em developer á. Mới trước họ ra mắt Github Open Copilot Chat thì mới đây lại thêm cái Using GitHub Copilot in the command line dùng để giải thích hoặc gợi ý lệnh trong terminal.

    Đây, cách dùng rất đơn giản thôi, ví dụ muốn nó giải thích câu lệnh sudo apt-get để làm gì, thì:

    $ gh copilot explain "sudo apt-get"

    Hoặc nhờ nó gợi ý lệnh mong muốn, sử dụng tiếng Việt được luôn nhé (kể cả tiếng Việt không dấu vẫn hiểu 😳)

    $ gh copilot suggest "xoá commit chưa push"

    Mình đã kiểm tra và thấy lệnh ra rất đúng, xịn thật 🤓

    » Xem thêm
  • Github có chương trình học và ôn thi để lấy chứng chỉ "ghim" vào hồ sơ cá nhân. Các chứng chỉ này xoay quanh kỹ năng sử dụng và làm việc thành thạo với git cũng như Github. Nếu bạn muốn học thêm kỹ năng mới đồng thời thu thập thêm badge cho mình thì còn chần chừ gì nữa 👇

    Showcase your expertise with GitHub Certifications

    » 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();
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 tuần trước
quá hay bạn ơi
Trả lời
Avatar
Xuân Hoài Tống5 ngày trước
Cảm ơn bạn Cường nha 🙏