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
  • 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
  • CloudFlare đã giới thiệu tính năng pay per crawl để tính phí cho mỗi lần AI "cào" dữ liệu trên trang web của bạn. Là sao ta 🤔?

    Mục đích của SEO là giúp các công cụ tìm kiếm nhìn thấy trang web. Khi người dùng tìm kiếm nội dung mà có liên quan thì nó hiển thị trang web của bạn ra kết quả tìm kiếm. Điều này gần như là đôi bên cùng có lợi khi Google giúp nhiều người biết đến trang web hơn, còn Google thì được nhiều người dùng hơn.

    Bây giờ cuộc chơi với các AI Agents thì lại khác. AI Agents phải chủ động đi tìm kiếm nguồn thông tin và tiện thể "cào" luôn dữ liệu của bạn về, rồi xào nấu hay làm gì đó mà chúng ta cũng chẳng thể biết được. Vậy đây gần như là cuộc chơi chỉ mang lại lợi ích cho 1 bên 🤔!?

    Nước đi của CloudFlare là bắt AI Agents phải trả tiền cho mỗi lần lấy dữ liệu từ trang web của bạn. Nếu không trả tiền thì tôi không cho ông đọc dữ liệu của tôi. Kiểu vậy. Hãy chờ thêm một thời gian nữa xem sao 🤓.

    » Xem thêm
  • Lúc khái niệm "Vibe Code" bùng nổ mình cũng tò và tìm hiểu xem nó là gì. Hoá ra là chỉ cách lập trình mới: Lập trình viên ra lệnh và để cho LLM tự viết mã. Sau đó là hàng loạt các bài viết nói về cách họ đã xây dựng ứng dụng mà không cần phải viết một dòng mã nào, hoặc 100% là do AI viết...

    Mình không có ý kiến gì vì mỗi người một sở thích. Nhưng nếu tiếp xúc với nhiều thông tin như vậy thì ít nhiều thế hệ lập trình viên mới sẽ "ám ảnh". Khi làm việc với ngôn ngữ lập trình, chúng ta đang tiếp xúc ở bề nổi rồi. Đằng sau đó còn nhiều lớp khác che giấu sự phức tạp. Ví dụ biết viết JavaScript nhưng có biết nó chạy như thế nào không 🤔? Trên thực tế bạn chẳng cần phải biết nó chạy như thế nào mà chỉ cần biết cú pháp là viết được chương trình chạy ngon ơ.

    LLMs giờ đây lại thêm một lớp ảo hoá cho việc viết mã. Tức là nơi chúng ta không cần trực tiếp viết mà là ra lệnh. Làm việc sẽ nhanh hơn nhưng khi gặp vấn đề thì nhiều khả năng phải vận dụng kiến thức của tầng thấp hơn để giải quyết.

    Mình dùng Cursor, nhưng tính năng thích nhất và dùng nhiều nhất là Autocomplete & Suggestions. Thi thoảng cũng dùng Agents để bảo nó viết tiếp đoạn mã đang dở, thường thì nó làm rất tốt. Hoặc khi gặp lỗi thì hỏi, có lúc giải quyết được, lúc thì không. Nhìn chung nó đang làm thay nhiệm vụ của Google & Stack Overflow, giúp tiết kiệm thời gian 😆

    LLMs như một cuốn bách khoa toàn thư rất khủng khiếp. Hỏi gì cũng biết, cũng trả lời được nhưng có một sự thật là nó chỉ là mô hình đoán chữ (đoán tokens). Thế nên nếu vấn đề phổ biến thì nó sẽ làm rất tốt, nhưng vấn đề ít phổ biến hơn thì nó lại rất tệ, hoặc thậm chí là đưa ra thông tin sai lệch, nhiễu... Tóm lại, cần phải biết cách khai thác thông tin, mà để biết thì buộc người dùng phải có một lượng kiến thức nhất định, tránh rơi vào cái bẫy thiên kiến uy quyền (tin tưởng tuyệt đối vào ai đó) hoặc thiên kiến xác nhận (xác nhận niềm tin sẵn có bằng cách chỉ tìm bằng chứng xác nhận niềm tin đó).

    Tại thấy bài viết này nên lại nổi hứng viết vài dòng 🤓 Why I'm Dialing Back My LLM Usage

    » 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

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? 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...