Nodejs | event loop | request |
Bài viết mang cái nhìn cá nhân. Các kiến thức ngoài phần tham khảo ở các tài liệu khác còn lại do mình tự suy luận, nên có thể mắc những sai sót nhất định. Hy vọng có thể nhận được sự góp ý của mọi người để bài viết được hoàn chỉnh cũng như mình có thể bổ sung thêm phần kiến thức.
Như chúng ta đã biết về single-thread
, non-blocking
, asynchronous
, concurrent
là các từ để miêu tả về JS. call stack
, event loop
, callback queue
là những gì có trong JS. Nodejs được xây dựng bởi JS, hiển nhiên nó có tất cả những thứ trên. Trong bài viết này, chúng ta sẽ đi tìm hiểu về cách thức các thành phần trên hoạt động trong Nodejs
1. Cùng đi qua một vài vấn đề nào?
V8 engine
Bao gồm call stack
và memory heap
-
memory heap
: vùng nhớ được dùng để lưu kết quả được tính toán ở các hàm trongcall stack
. Cũng như trongC++
nó có thể được cấp phát tĩnh hoặc cấp phát động. -
call stack
: Hoạt động theo đúng nghĩa stack LIFO - Last In First Out. Khi chương trình thực thi đến hàm nào, thì hàm đó sẽ đẩy vào trongcall stack
. Và nó chỉ được lấy ra khi đã hoàn thành và return.
Phần V8 engine này các bạn có thể đọc và tìm hiểu ở bất kỳ tài liệu nào, rất cụ thể và chi tiết.
Web APIs
Không phải là thành phần của JS, nó là tiện ích của Browser, chứa các hàm như setTimeout(), fs.readFile(), emitter…
Không phải bất cứ câu lệnh nào cũng được thực thi ngay trên call stack
, ví dụ như những hàm trên sẽ được đưa sang Web APIs khi gọi đến.
Callback Queue
Nơi chứa các hàm ở Web APIs xuống. Nó sẽ ở đây chờ đến khi nào call stack
rỗng để nhảy lên và thực thi tiếp các câu lệnh bên trong hàm.
Event Loop
Là thành phần của libuv
nó chính là vòng lặp để xử lý các sự kiện. Các hàm trong Callback Queue nhờ vòng lặp này sẽ được bế lên call stack
khi rỗng để tiếp tục thực thi.
OK, có lẽ cơ bản như thế là đủ. Các bạn hoàn toàn có thể tìm trên mạng rất nhiều bài nói chi tiết về các vấn đề trên.
Tham khảo thêm:
2. Bức tranh trong môi trường Nodejs?
Theo dõi video và link demo trên chúng ta thấy khá rõ ràng về cách thức JS thực thi các hàm. Nhưng trong môi trường server Nodejs, chúng ta có các request, response đồng thời thì cơ chết hoạt động sẽ ra sao?
Cơ chế xử lý request trong Nodejs
Mình nghĩ đó là những cái basic nhất về cơ chế hoạt động của Nodejs.
Vì sao JS đơn luồng
Vì nó chỉ có một event queue
, một call stack
, một event loop
, một callback queue
. Đối với các ngôn ngữ khác như Java, thì mỗi request được gửi từ client, server sẽ có 1 thread riêng để xử lý request đó. Như vậy các request là song song đồng thời. Nhưng ở Nodejs thì không như vậy, vì nó chỉ có đơn luồng, việc dùng chung các tài nguyên hẳn là sẽ xảy ra các tranh chấp. Nhưng tại sao, ở nhiều bài viết trên mạng, mọi người vẫn nói Nodejs có thể xử lý hàng triệu kết nối cùng lúc 😇. Cùng đi tìm hiểu nào.
Bắt đầu
- Khi có nhiều request được gửi từ nhiều client trong cùng một thời điểm, các request này sẽ được đưa vào
event queue
hoạt động theo cơ chế FIFO - First In First Out. - request được lấy ra ở
event queue
sẽ được đưa đếncall stack
để xử lý. Ở đây, trên server sẽ thực thi các hàm để có kết quả trả về cho request đó - giống như thực thi hàm có trong video tham khảo ở phần 1. - Như vậy, tại 1 thời điểm Nodejs chỉ có thể phục vụ được 1 request duy nhất, vậy nếu request này xử lý quá lâu như cần query vào DB, thì hiển nhiên các request khác trong
event queue
sẽ phải đợi và có thể dẫn đến timeout. - Nếu chỉ có mỗi V8 engine thì sẽ xảy ra vấn đề như vậy. Nên Nodejs cần thêm một số thành phần khác. Đó là
Node APIs
,libuv
.
Bước thứ 2
- Rõ ràng nếu chỉ có V8 engine thì nó không thể thực hiện query vào DB được. Vì V8 engine được thiết kế ra chỉ để thực hiện các phép toán, thực thi hàm, cung cấp data type, object… và GC(thành phần rọn rác cho bộ nhớ).
- Đa phần khi server nhận được request, sẽ phải query vào trong DB, lấy ra dữ liệu, xử lý dữ liệu đó rồi trả về cho client. Đó gần như là một vòng khép kín.
- Khi trong
call stack
thực thi một hàm của một request, hàm này cần phải query dữ liệu trong DB. Vì query vào DB là hoạt độngI/O
được quy định trongNode APIs
. Do vậy nó không thể thực thi trêncall stack
được. Giống nhưsetTimeout()
có trong ví dụ phần 1. Nó sẽ được thực thi ở một nơi khác.
Nơi khác là nơi nào?
- Làm thế nào để đoạn mã trong
call stack
biết được nó có đượcNode APIs
đảm nhiệm không? - Theo mình nghĩ chỗ này sẽ có một
adapter
chuyên check việc hàm đó có hay không trongNode APIs
. - Nếu hàm không thực thi một tác vụ liên quan đến
I/O
thìadapter
sẽ trả vềfalse
. Ví dụ, request yêu cầu lấy thời gian của server, khi đó chỉ cầnreturn new Date()
rồi response về cho client. Như vậy hàm mà córeturn new Date()
không liên quan đếnNode APIs
nên nó sẽ được xử lý ngay trongcall stack
với thời gian rất nhanh và trả về cho client luôn. - Nếu nó thực thi một tác vụ liên quan đến
I/O
thìadapter
sẽ trả vềtrue
. Ví dụ như việc query vào DB, sẽ cần một thời gian nhất định. Khi đóNode APIs
biết mình cần phải xử lý cái hàm đó. Nó sẽ lấy ra khỏicall stack
để chocall stack
thực thi các hàm khác. Đồng thời phân cho 1 thread có trongthread pool
để xử lý tác vụI/O
trong hàm vừa lấy ra. Chỗ này là bắt đầu đa luồng rồi nhé lại quay về kiến thức của Java 😆 - Luồng này sẽ được xử lý trong
handle thread
, lấy dữ liệu trong DB, sau khi xong sẽ được đẩy xuốngcallback queue
chờ ngày được lêncall stack
😆 - Khi đó
event loop
bắt đầu nhiệm vụ. Kiểm tra nếucall stack
rỗng nó sẽ đưa hàm ởcallback queue
lên và thực thi các câu lệnh tiếp theo. Sau khi xong nó sẽ return về kết quả và response về cho client và câu chuyện đến đây là hết rồi 😂.
Đa luồng vẫn có trong Nodejs
- Phân tích ở phía trên với chỉ 1 request. Còn nếu nhiều request đồng thời thì Nodejs vẫn cần đa luồng để xử lý các tác vụ
I/O
. - Khi 1 request cần query vào DB để lấy dữ liệu. Nó sẽ được một thread nào đó đảm nhiệm. Khi có
call stack
rỗng nó sẽ tiếp nhận các request tiếp theo trongevent queue
để xử lý tiếp. Nếu request này tiếp tục chứa tác vụI/O
thì sẽ được một thread khác đảm nhiệm tiếp. Đó là lý do chúng ta cóthread pool
- một thành phần củalibuv
. - Chúng ta có thể lợi dụng điểm này để chạy các tác vụ cùng query 1 lúc trên nhiều thread khác nhau bằng việc sử dụng
Promise.all()
. - Các hàm có trong
Node APIs
đều được xử lý dựa vào đa luồng có trongthread pool
. Nếu nói Nodejs có đa luồng không, thì mình nghĩ trong những trường hợp nhất định, Nodejs vẫn có đa luồng.
3. Kết luận gì ở đây.
- Để server Nodejs của bạn có thể đáp ứng được nhiều kết nối từ client, hay đảm bảo
call stack
xử lý các hàm nhanh nhất có thể. Luôn luôn có pool trongthread pool
sẵn sàng đảm nhiệm việc thực thi hàm. - Bạn có thể hay nghe đến
block luồng chính
có nghĩa là hàm nào đó thực thi quá lâu trongcall stack
dẫn đến các request không được thực thi. Do vậy, khi code cần rất chú ý đến vấn đề này. - Nếu vừa có dữ liệu ở
callback queue
và có request ởevent queue
trong thời điểmcall stack
rỗng thì sẽ xảy ra tranh chấp tài nguyên. Nodejs đã xử lý như nào? Theo mình nghĩ Nodejs sẽ ưu tiên lấy trongcallback queue
trước để hoàn thành request cho client. Cái này mình cũng chưa thử tìm hiểu. - Nhưng nếu request của bạn thực thi 1 phép tính quá phức tạp, thì nó sẽ thực hiện ngay trên
call stack
và hiển nhiên các request khác sẽ phải đợi cho đến khi request đang chiếmcall stack
được thực thi xong. Điều là là tối kỵ trong Nodejs. Nếu phải thực hiện một phép tính phức tạp nào đấy. Hãy tìm phương án tối ưu hơn như đẩy sang 1 worker để xử lý.