Kiến trúc Node.js - Event Loop

Kiến trúc Node.js - Event Loop

Tin ngắn hàng ngày dành cho bạn
  • Kết thúc kỳ nghỉ, để thấy nó dữ dội đến đâu thì tổng số bước đã đi trong 4 ngày là 45k các bạn ạ 😆

    » Xem thêm
  • Đây là bài viết thứ 366, có nghĩa là mình đã duy trì được việc mỗi ngày một bài trong chuyên mục Threads 1 năm rồi đấy mọi người. Tuy rằng không phải ngày nào cũng viết vì nhiều hôm mình bận, quên thì hôm sau là lên bài bù, mục đích là để cam kết với độc giả, ấy vậy mà quay đi quay lại đã một năm trôi qua rồi. Nhanh thật 😃

    À mai, kia, ngày kìa nữa mình đi du lịch nên chắc không lên bài cho mọi người được. Về rồi mình lên sau nhé 😅. Cảm ơn!

    » Xem thêm
  • Hơn 1 tuần nay mình không đăng bài, không phải không có gì để viết mà đang tìm cách để phân phối nội dung có giá trị hơn trong thời đại AI đang bùng nổ mạnh mẽ như thế này.

    Như từ hồi đầu năm đã chia sẻ, số lượng người truy cập vào trang blog của mình đang dần ít đi. Khi xem thống kê, lượng người dùng trong 6 tháng đầu năm 2025 đã giảm 30% so với cùng kì năm ngoái, 15% so với 6 tháng cuối năm 2024. Như vậy một sự thật là người dùng đang rời bỏ dần đi. Nguyên nhân do đâu?

    Mình nghĩ lý do lớn nhất là thói quen của người dùng đã thay đổi. Họ tìm thấy blog chủ yếu qua các công cụ tìm kiếm, trong đó lớn nhất là Google. Gần 1/2 số lượng người dùng quay trở lại blog mà không cần thông qua bước tìm kiếm. Đó là một tín hiệu đáng mừng nhưng vẫn không đủ để tăng lượng người dùng mới. Chưa kể giờ đây, Google đã ra mắt tính năng AI Search Labs - tức là AI hiển thị luôn nội dung tổng hợp khi người dùng tìm kiếm, điều đó càng khiến cho khả năng người dùng truy cập vào trang web thấp hơn. Một điều thú vị là khi Search Labs được giới thiệu, thì các bài viết bằng tiếng Anh đã soán ngôi trong bảng xếp hạng truy cập nhiều nhất.

    Một bài viết của mình thường rất dài, có khi lên đến cả 2000 chữ. Mà để viết ra được một bài như thế tốn nhiều thời gian. Nhiều bài viết ra chẳng có ai đọc là điều bình thường. Mình biết và chấp nhận vì không phải ai cũng gặp phải vấn đề đang nói đến. Viết đối với mình như một cách để rèn luyện sự kiên nhẫn và cả tư duy. Viết ra mà giúp được cả ai đó là một điều tuyệt vời.

    Vậy nên mình đang nghĩ sẽ tập trung vào nội dung ngắn và trung bình để viết được nhiều hơn. Nội dung dài chỉ khi muốn viết chi tiết hoặc đi sâu về một chủ đề nào đó. Nên là đang tìm cách thiết kế lại trang blog. Mọi người cùng chờ nha 😄

    » Xem thêm

Vấn đề

Trong bài viết trước chúng ta đã biết Event loop là một thành phần quan trọng trong JavaScript/Node.js. Nhiệm vụ của nó là mang các hàm callback ở trong Callback queue quay trở lại call stack. Nhưng câu chuyện chưa dừng lại ở đó, Event loop có cách mang các hàm quay trở lại khác nhau. Hay nói cách khác là các hàm callback có thứ tự ưu tiên khác nhau. Độ ưu tiên càng lớn thì Event loop càng mang nó quay trở lại call stack nhanh hơn. Hiểu được cơ chế này bạn đọc sẽ viết ra được chương trình tối ưu về mặt hiệu suất.

Trước tiên hãy tìm hiểu các pha (phases) của Event loop để biết thứ tự ưu tiên của các hàm callback.

Các pha (Phases) của Event loop

Hãy tưởng tượng Event loop như một bánh xe chia thành nhiều ngăn, cụ thể ở đây là 6. Để quay hết một vòng cần phải đi qua đủ 6 ngăn. Ở mỗi ngăn, Event loop kiểm tra xem có hàm callback nào đáp ứng tiêu chí thì sẽ "bốc" nó quay trở lại call stack.

Dưới đây là sơ đồ các pha của vòng lặp sự kiện.

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

Bây giờ hãy đi vào chi tiết từng pha.

timers

Timers là nơi xử lý các callback của setTimeoutsetInterval. Khi thời gian chờ được thiết lập đã trôi qua, callback tương ứng sẽ được thực thi tại đây.

Cũng cần phải lưu ý rằng đây là thời gian chờ tối thiểu, không phải chính xác tuyệt đối do phụ thuộc vào độ trễ của các pha khác.

Ví dụ.

const fs = require('node:fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();
  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

Giả sử someAsyncOperation mất 95ms để hoàn thành, nó nhỏ hơn thời gian chờ 100ms của hàm setTimeout. Về lý thuyết, lệnh console.log trong setTimeout in ra delay bằng 100ms đúng với thời gian chờ nhưng hãy để ý hàm callback của someAsyncOperation đang "kẹt" ở pha pool (lệnh while đã kịp đưa vào call stack) nên mất một độ trễ nhất định thì callback trong setTimeout mới được đưa vào call stack. Lúc này chúng ta thấy thời gian delay lớn hơn 100ms vài đơn vị.

pending callbacks

Pending callbacks xử lý các callback cho một số hoạt động hệ thống như các loại lỗi TCP, lỗi trong hệ thống tệp I/O (như fs).

idle, prepare

Idle, prepare là giai đoạn sử dụng nội bộ của Node.js nhiều hơn là mã của người dùng.

poll

Poll là giai đoạn trung tâm của xử lý bất đồng bộ. Poll chờ và xử lý hầu hết các sự kiện I/O như đọc/ghi tệp tin, kết nối mạng,... Poll có hai chức năng chính: Tính toán thời gian cần chặn và Xử lý các sự kiện trong hàng đợi poll.

Khi vòng lặp sự kiện vào đến giai đoạn poll, nó thực hiện 2 công việc:

  • Nếu có callback I/O: thực thi ngay.
  • Nếu không có callback và:
    • Có callback setImmediate: kết thúc poll, chuyển sang pha check.
    • Không có setImmediate và chưa hết thời gian chờ: chờ tiếp.

Poll có một ngưỡng thời gian chờ nhất định khi nó đang trống trước khi chuyển sang giai đoạn tiếp theo.

check

Check xử lý các callback được đăng ký qua setImmediate.

close callbacks

Close callbacks xử lý các callback khi một tài nguyên kết nối đóng đột ngột. Ví dụ socket.destroy() hoặc sự kiện close được phát ra.

Ngoài 6 pha trên, Node.js còn có một "pha đặc biệt" nữa là process.nextTick().

process.nextTick()

process.nextTick() không được hiển thị trong sơ đồ 6 pha của vòng lặp sự kiện mặc dù nó là một phần của API không đồng bộ. Điều này là do về mặt kỹ thuật nó không phải là một phần của vòng lặp sự kiện. Thay vào đó, hàm callback của process.nextTick() sẽ được xử lý sau khi hoạt động của pha hiện tại hoàn tất. Hay nói cách khác, nó luôn được ưu tiên xử lý đầu tiên mỗi khi vòng lặp sự kiện bước vào pha tiếp theo.

Cũng chính vì process.nextTick() luôn được thực thi trước khi bước vào pha nên nó có khả năng chặn vòng lặp sự kiện như ví dụ dưới đây.

function endlessLoop() {
  process.nextTick(endlessLoop);
}
endlessLoop();

Trong bài viết tiếp theo, chúng ta hãy đi sâu vào tìm hiểu process.nextTick() nhé.

Kết luận

Event Loop là một thành phần cốt lõi trong kiến trúc của Node.js, đóng vai trò điều phối các tác vụ bất đồng bộ bằng cách đưa các hàm callback từ hàng đợi (Callback queue) vào Call Stack để thực thi. Trong bài viết này, chúng ta đã khám phá 6 pha chính của Event Loop, bao gồm timers, pending callbacks, idle, prepare, poll, check, và close callbacks, mỗi pha đảm nhận nhiệm vụ xử lý các loại callback khác nhau. Timers chịu trách nhiệm cho các hàm setTimeoutsetInterval, trong khi Poll là trung tâm xử lý các sự kiện I/O và quyết định chuyển tiếp giữa các pha. Ngoài ra, setImmediate trong pha Check và các callback đột ngột trong Close đều có vai trò riêng biệt trong cơ chế vận hành. Đặc biệt, process.nextTick() tuy không thuộc Event loop nhưng lại có mức độ ưu tiên cao nhất, đảm bảo callback của nó luôn được thực thi ngay lập tức sau pha hiện tại.

Hiểu rõ cơ chế hoạt động của Event Loop và thứ tự ưu tiên của từng pha là chìa khóa để tối ưu hóa hiệu suất chương trình Node.js. Điều này giúp bạn kiểm soát tốt hơn các tác vụ bất đồng bộ, từ đó giảm thiểu độ trễ và tối ưu hóa trải nghiệm tổng thể. Trong bài viết tiếp theo, chúng ta sẽ đi sâu hơn vào process.nextTick() để khai thác triệt để tiềm năng mà Node.js mang lại.

Tham khảo:

Cao cấp
Hello

Bí mật ngăn xếp của Blog

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, 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 (0)

Nội dung bình luận...