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
  • Manus đã chính thức mở cửa cho tất cả người dùng rồi đấy mọi người. Cho những ai chưa biết thì đây là một công cụ viết báo cáo (làm mưa làm gió) giống như Deep Research của OpenAI á. Mỗi ngày được miễn phí 300 Credits để nghiên cứu. Mỗi lượt nghiên cứu tiêu tốn tuỳ thuộc vào độ phức tạp của yêu cầu. À với cả họ đang có chương trình tặng miễn phí Credits hay sao á. Như mình thì vào thấy được hẳn 2000.

    Mình dùng thử, so sánh với cùng một lệnh giống như đợt trước dùng bên Deep Research thì nội dung khác biệt nhau hoàn toàn. Manus báo cáo như kiểu viết văn hơn so với OpenAI là các gạch đầu dòng và bảng biểu.

    À lúc đăng ký xong có bắt nhập số điện thoại để xác minh, nếu lỗi thì các bạn đợi qua ngày thử lại xem có được không nhé.

    » Xem thêm
  • Mọi người chắc nghe nhiều về xu hướng tìm kiếm thông tin bằng AI chứ không cần công cụ tìm kiếm như Google nữa rồi đúng không? Không đâu xa ánh xạ vào bản thân thì thấy đúng thật, thi thoảng mới tìm kiếm thôi chứ còn đâu toàn hỏi tụi AI.

    Ngay từ đầu viết blog, thứ mà mình hướng đến là chia sẻ kinh nghiệm chứ không phải là những bài mang nặng tính kỹ thuật, máy móc, hướng dẫn từ đầu... Vì thời điểm đó đã có quá nhiều người làm nội dung này rồi và họ làm rất tốt, tại sao mình phải cố phát minh lại bánh xe? Một điều nữa là tin tưởng độc giả của mình có khả năng tìm hiểu vấn đề. Nếu bạn đọc đủ nhiều các bài viết trên blog thì thấy mình luôn cố gắng chèn thêm các liên kết tham khảo ngoài bài viết, nêu ra vấn đề mở và rất ít khi kết luận chắc chắn một điều gì đó.

    Mình đã cố gắng rèn luyện kỹ năng viết, kỹ năng trình bày và cả cách tương tác với độc giả để mang lại giá trị cho họ. Nhiều lúc ngồi lật lại các con số thống kê thấy lượng đọc bài viết tăng lên lại cảm thấy vui. Nhưng khi nguồn truy cập đến từ Google thì lại thấy buồn, vì điều đó chứng tỏ họ biết đến mình chỉ khi đang cố đi tìm giải pháp, có thể họ chỉ đọc chớp nhoáng, may ra tìm được cách giải quyết và thế là đóng cửa sổ trình duyệt rồi đi như một cơn gió.

    Chừng vài tháng đổ lại đây, một điều khiến mình rất vui đó là lượng người truy cập thẳng vào trang chủ mà không thông qua công cụ tìm kiếm đang tăng dần lên, có nhiều hôm lượng truy cập tự nhiên còn cao hơn cả đến từ Google. Điều đó chứng tỏ độc giả đã có thói quen quay lại trang của mình nhiều hơn và họ tìm thấy được giá trị từ blog mang lại. Vui mừng khôn xiết 🤩

    Bên cạnh đó thì lượng truy cập vào chuyên mục Threads - tức là mục mình đang viết bài này đang cao hơn bao giờ hết. Điều đó chứng tỏ xu hướng đi theo tin nhanh là đúng đắn. Mình có thể ngồi cả ngày để viết tin ngắn cho bạn đọc vì nó rất nhanh mà tiện, không tốn công đi tìm tài liệu để viết, không tốn cả thời gian viết nữa, còn mình thì có rất nhiều thứ để chia sẻ 😅. Nhưng không vì thế mà bỏ bê các bài viết dài, vì dài thì có nhiều thông tin để chia sẻ hơn.

    Vài lời tâm sự thế thôi chứ hơn một tháng nay mình chưa viết bài viết mới nào vì công việc bận quá. Xong lâu dần cứ trì hoãn lại thành lười. À với cả tháng 5 này rất thích hợp để đọc các cuốn sách về cách mạng á. Có hôm đọc đến 2 giờ sáng mới đi ngủ 🥱

    » Xem thêm
  • Mình mới nhìn thấy một trang web khá thú vị nói về các cột mốc đáng nhớ trong lịch sử phát triển Internet toàn cầu: Internet Artifacts

    Chỉ từ 1977 - khi Internet còn nằm trong hộp thí nghiệm thì nhìn xem - giờ đây Internet đã khiến mọi thứ phát triển đến mức nào 🫣

    » 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...