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

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