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
  • Void - cái tên mình đã nhắc đến từ cách đây khá lâu. Từ đợt mà continue.dev mới nổi lên á. Nó tương tự như Cursor và Windsurf, mới hôm nay họ đã phát hành phiên bản Beta và cho phép mọi người tải xuống.

    Điểm mạnh thì đây là nguồn mở, miễn phí, dùng các mô hình miễn phí cục bộ trên máy qua Ollama hoặc LM Studio... Không thích thì cắm API của bên khác vào cũng được. Mình vừa dùng thử thì thấy khả năng gợi ý và khung chat khá tương đồng với Cursor, có cả tính năng Agent luôn nhé 👏. Hoạt động ổn định hơn continue.dev (lần cuối dùng), việc còn lại là chọn mô hình xịn xịn tí 🤤

    » Xem thêm
  • Zed mới đây đã giới thiệu thêm tính năng Agent - tương tự như Agent trong Cursor hay Write trong Windsurf và họ gọi nó là The Fastest AI Code Editor.

    Cũng nhanh thật đấy vì Zed viết bằng Rust. Cơ mà chiến lược của họ có vẻ thay đổi, tập trung vào AI thay vì phát triển kho tiện ích mở rộng vốn đang có rất ít, không thể cạnh tranh được với VSCode 🥶

    Zed: The Fastest AI Code Editor

    » Xem thêm
  • Ngay sau thông tin OpenAI đạt được thoả thuận mua lại Windsurf với giá 3 tỉ đô thì ngày hôm nay Cursor đã miễn phí 1 năm dùng bản Pro cho sinh viên. Chaaaaà 🤔

    OpenAI Reaches Agreement to Buy Startup Windsurf for $3 Billion

    Cursor for Students | Cursor - The AI Code Editor

    » 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

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 (0)

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