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
  • 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 đề

Ở 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

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