Kiến trúc Node.js - Node.js xử lý bất đồng bộ như thế nào?

Kiến trúc Node.js - Node.js xử lý bất đồng bộ như thế nào?

Tin ngắn hàng ngày dành cho bạn
  • Không hề kém cạnh, Google mới đây đã giới thiệu Gemini CLI - Một dạng AI Agent tương tự như Codex hay Claude Code.

    Điều đáng lưu ý là họ cho dùng miễn phí tới... 1000 truy vấn mỗi ngày. Nhiều đấy chứ. Ngoài ra họ cũng mã nguồn mở dự án này để đảm bảo tính minh bạch, học tập và nghiên cứu 🤓

    » Xem thêm
  • Lại có thêm một công cụ hỗ trợ tìm kiếm nhanh lịch sử gõ lệnh nè mọi người: atuinsh/atuin.

    Điều thú vị là nó dùng SQLite để lưu trữ. Ngoài ra còn cung cấp tính năng đồng bộ hóa (mã hóa) hoàn toàn lịch sử giữa các máy với nhau nữa. Hay ghê 🤓

    » Xem thêm
  • Mình thấy ấn tượng với mô hình gemma-3n-E4B của nhà Google ghê. Đây là một trong những mô hình hứa hẹn mang các mô hình ngôn ngữ lớn xuống chạy trên thiết bị di dộng hoặc web hoặc nhúng (embedded)...

    Cảm giác nó hiểu lời nhắc hơn á, tại vì mình thử nhiều mô hình ít tham số mà nó hay lơ đi lời nhắc của mình. Ví dụ bảo: "Chỉ trả về câu trả lời, không cần giải thích gì thêm" thì rất nhiều cái vẫn cứ phải chêm vào câu mở đầu, giải thích... còn với gemma-3n thì trả lời rất đúng trọng tâm.

    » Xem thêm

Vấn đề

Ở bài viết trước chúng ta đã biết JavaScript/Node.js chỉ có một luồng chính để thực thi mã JavaScript. Biết thế nào là tác vụ I/O đồng bộ, không đồng bộ. JavaScript/Node.js đã khéo léo lựa chọn cách xử lý không đồng bộ để tránh làm mất thời gian xử lý của call stack. Bài viết ngày hôm nay chúng ta sẽ tìm hiểu Node.js xử lý bất đồng bộ như thế nào.

Trước tiên hãy làm quen với một thành phần mới: Libuv

Libuv

Quay trở lại với ví dụ trong phần trước.

fs.readFile(file.pdf)
  .then(pdf => console.log("pdf size", pdf.size));

fs.readFile(file.doc)
  .then(doc => console.log("doc size", doc.size));

Thứ tự các hàm được đưa vào call stack như sau.

+------------------------------+
|                              |
| fs.readFile(file.pdf)        |
+------------------------------+
|        Call stack            |
|------------------------------|
             |
             v
+------------------------------+
|                              |
| fs.readFile(file.doc)        |
+------------------------------+
|        Call stack            |
|------------------------------|

Vì kết quả của hàm bất đồng bộ không trả về ngay lập tức nên call stack thực hiện 2 lệnh trên rất nhanh. Vậy kết quả của 2 hàm trên được xử lý ở đâu và như thế nào? Hay nói cách khác là phần mã trong then đi đâu?

Libuv là nơi xử lý I/O không đồng bộ. Libuv là một thư viện ngoài được Node.js lựa chọn sử dụng để xử lý I/O. JavaScript trong trình duyệt không dùng Libuv mà triển khai thứ gọi là Web APIs.

Call stack gặp các hàm bất đồng bộ ngay lập tức đẩy chúng vào libuv hoặc Web APIs, nên gần như nó không xử lý gì ở đây cả. Sau khi có kết quả của hàm bất đồng bộ, libuv gắn kết quả vào hàm callback, cũng như hàm ở trong then (để ý callback hoặc then luôn là một hàm có tham số, tham số đó là kết quả của libuv trả về), đẩy nó vào một nơi gọi là Callback Queue.

Vậy callback queue sau khi có kết của libuv thì làm gì tiếp theo?

Event loop

Event loop là một thành phần quan trọng trong JavaScript/Node.js, nó là một vòng lặp vô tận không bao giờ dừng khi chạy chương trình. Nhiệm vụ của Event loop là mang các hàm callback ở trong Callback queue quay trở lại call stack.

Event loop luôn luôn theo dõi call stack bởi vì chỉ khi call stack trống nó mới bắt đầu nhiệm vụ chuyển callback ở trong Callback queue vào call stack. Đó cũng là lý do cho ví dụ ở phần một in ra World Hello.

setTimeout(function() {
  console.log("Hello");
}, 0);

console.log("World");

setTimeout với giá trị 0 gần như là không có độ trễ, vậy tại sao không in luôn Hello ra trước World? Dễ thôi, hàm callback trong setTimeout đã được call stack đẩy ra libuv hoặc Web APIs trước khi Event loop kịp mang nó quay trở lại call stack.

Event Loop chỉ có một nhiệm vụ duy nhất là mang các hàm callback ở trong Callback queue quay trở lại call stack. Nó không trực tiếp thực thi mã JavaScript. Mã chỉ được chạy khi nó được đưa vào call stack. Nếu call stack không trống, các hàm trong Callback queue không bao giờ được mang trở lại call stack. Vì thế mà người ta hay nói đừng bao giờ chặn vòng lặp sự kiện. Nếu chặn, chương trình trở nên chậm chạp vì yêu cầu không bao giờ có kết quả để trả về cho người dùng.

Sơ đồ thể hiện việc Node.js xử lý bất đồng bộ

Nhờ sự có mặt của các thành phần quan trọng là libuv, Web APIs, Callback Queue, Event Loop... mà Node.js xử lý được kết quả của hàm bất đồng bộ. Hành động đó có thể tóm lại trong sơ đồ sau.

+------------------------------+
|         Call Stack           |
|----------------------------- |
| Gặp phải hàm bất đồng bộ     |
|                              |
|                              |
| -> Đẩy vào libuv/Web APIs    |
+------------------------------+
             |
             v
+------------------------------+
|      libuv/Web APIs          |
|----------------------------- |
| Xử lý I/O                    |
| Khi xong, đưa callback vào   |
| hàng đợi (Callback Queue)    |
+------------------------------+
             |
             v
+------------------------------+
|        Callback Queue        |
|----------------------------- |
| [kết quả I/O và callback]    |
|                              |
+------------------------------+
             |
             v
+------------------------------+
|          Event Loop          |
|----------------------------- |
| Kiểm tra: Call Stack rỗng?   |
| Nếu rỗng, đẩy callback từ    |
| Callback Queue lên Call Stack|
+------------------------------+
             |
             v
+------------------------------+
|         Call Stack           |
|----------------------------- |
| thực thi hàm callback        |
+------------------------------+

Kết luận

Node.js với kiến trúc single-thread đã chọn cách xử lý bất đồng bộ để tối ưu hóa hiệu năng, tránh việc call stack bị chặn bởi các tác vụ I/O. Cơ chế này dựa vào sự phối hợp giữa các thành phần quan trọng: libuv (thư viện hỗ trợ xử lý I/O không đồng bộ) hoặc Web APIs, Callback Queue (hàng đợi chứa các hàm callback sau khi I/O hoàn tất) và Event Loop (vòng lặp sự kiện đảm nhiệm việc đưa callback trở lại call stack khi call stack trống). Khi một tác vụ bất đồng bộ được gọi, Node.js đẩy nó sang libuv hoặc Web APIs để xử lý. Sau khi hoàn thành, kết quả và hàm callback được đưa vào Callback Queue, chờ Event Loop chuyển lên call stack để thực thi. Chính nhờ sự vận hành nhịp nhàng này mà Node.js đạt được khả năng xử lý hiệu quả các tác vụ bất đồng bộ mà không làm gián đoạn luồng chính.

Tóm lại, sức mạnh của Node.js trong việc xử lý bất đồng bộ nằm ở sự kết hợp giữa libuv, Callback Queue và Event Loop. Điều này cho phép Node.js duy trì hiệu năng cao, nhưng cũng đòi hỏi lập trình viên phải cẩn trọng để không vô tình chặn vòng lặp sự kiện, khiến chương trình mất đi tính tối ưu. Việc hiểu rõ cơ chế này giúp chúng ta vận dụng Node.js một cách hiệu quả hơn, đặc biệt khi làm việc với các tác vụ I/O phức tạp.

Ở bài viết tiếp theo, chúng ta hãy đi sâu hơn vào Event loop nhé.

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