Ở 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
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 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.
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 |
+------------------------------+
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é.
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!
Đăng ký nhận thông báo bài viết mới
Bình luận (0)