Node.js Advanced Concept : Event Loop (Part 1)

Trong những bài trước chúng ta đã hiểu cơ bản cách thức hoạt động của Node.js, mối liên hệ
giữa V8 và Libuv bên trong Node như thế nào, bài viết này mình sẽ đề cập đến một thứ gọi là Event Loop một thành phần cực kì quan trọng được triển khai bên trong Libuv có nhiệm vụ để xử lý các tác vụ bất động bộ, nhưng trước khi hiểu rõ cách thức hoạt động của Event Loop chúng ta cần hiển rõ một số khái niệm sau đây.



I. Threads.

Mỗi khi chúng chạy bất cứ chương trình nào trên máy tính thì một process sẽ bắt đầu chạy, một process như một instance của một ứng dụng vậy và mỗi process sẽ có nhiều thread, bạn hãy hình dung thread như một bản danh sách các việc cần làm, danh sách này sẽ được trao cho CPU và CPU sẽ thực thi nhiệm vụ theo danh sách này.
Như vậy mỗi khi chúng ta chạy một Node-App sẽ có 
+ 1 process được tạo ra.
+ 1 thread được tạo ra.
+ 1 Event loop được khởi tạo.
+ 1 Javascript Engine.
+ 1 Instance của Node.



Mỗi thread tại một thời điểm xác định chỉ thuộc về một process, một vấn đề quan trọng trong quá trình chạy một chương trình đó là hệ điều hành quyết định xử lý thread nào trong một process sẽ chạy vào thời điểm nào vấn đề này trong khoa học máy tính được gọi Schedule và được xử lý bởi một bộ phần gọi là OS Schedule.



Hãy nhớ rằng máy tính của bạn có một số lượng tài nguyên hạn chế, CPU có thể thực hiện hàng nghìn hướng dẫn trong một lúc,điều này là đương nhiên vì tại một thời điểm CPU sẽ phải xử lý rất nhiều process và thread.
Như hình bên trên bạn có thể thấy có những thread mà OS Schedule phải xử lý nó ngay không được chậm chễ như
+ Ghi nhận sự hoạt động của chuột.
+ Ghi nhận sự hoạt động của bàn phím.
Tưởng tượng xem sau 5s máy tính mới ghi nhận sự hoạt động của chuột , điều này là thảm họa về mặt UX, user sẽ nghĩ ứng dụng của bạn bị broke!!!.

=> Vậy làm thế nào để CPU xử lý nhanh hơn và nhiều hơn tại một thời điểm ??

Câu trả lời đơn giản nhất là : Tăng thêm nhân CPU cho máy tính.
Điều này mang lại 2  lợi ích :

+ Tăng khả năng xử lý nhiều luồng tại một thời điểm.
Nhưng mình cũng đề cập rằng một CPU Core cũng có khả năng xử lý nhiều thread tại một thời điểm gọi là Multithread hay bạn thường nghe nó với cái tên Hyper Threading.

+ Kiểm soát chặt chẽ việc đang được xử lý bởi mỗi thread.

II. Event Loop

Như mình đã nêu bên trên khi chúng ta chạy một Node-App, nó sẽ tạo ra một proccess mới và Node-App sẽ chỉ chạy trên 1 thread duy nhất trong process này.

Event Loop là thành phần quan trọng nhất trong mỗi ứng dụng, Event Loop là thứ giúp bạn xử lý các tác vụ không đồng bộ I/O nó làm nên tính chất bất đồng bộ của Node. bạn có thể nghĩ đơn giản Event Loop như một cấu trúc điều khiển một thread của chúng ta sẽ làm gì, được thực hiện vào thời điểm nào. Điều bạn cần chú ý rằng Node-App luôn luôn chỉ chạy trên duy nhất một thread chỉ có các hoạt động I/O được thực thi song song vì chúng là những hoạt động không đồng bộ.

Có một số quan niệm hết sức sai lầm mà nhiều người mắc phải với Node.js đó là Node là đơn luồng (Single thread) hay nói rõ hơn là Event Loop chỉ chạy trên một thread duy nhất, điều này là chính xác nhưng nó không phải toàn bộ câu chuyện đằng sau hậu trường của Node.js.

Hãy nhìn sơ đồ sau:


Như mình đã đề cập bên trên mỗi khi bạn khởi chạy một application Node một instance của EventLoop sẽ tạo và đặt vào bên trong một thread.
=> Điều này rõ ràng là không hay tý nào khi chúng ta không thể tận dụng tối đa các lõi của CPU.
Nhưng có điều đặc biệt trong Node đó là nó triển khai rất nhiều module và thư viện được triển khai và một trong số chúng không chạy đơn luồng.
=> Như vậy kết luận Node chạy đơn luồng hoàn toàn không phải là một kết luận chính xác, nói rõ ràng hơn EventLoop bên trong Node chạy đơn luồng nhưng một số code chúng ta viết ra không hoàn toàn thực thi trên cùng một thread với EventLoop.

Hãy triển khai đoạn code sau đây :
Hàm pbkdf2 được triển khai trong modult crypto, mình lấy nó để ví dụ cho một tác vụ bất đồng bộ mất nhiều thời gian để thực thi.



Hãy giả sử rằng Node-App của chúng ta chỉ chạy trên một thread duy nhất, vậy điều gì sẽ xảy ra ?


Giả sử hàm thứ nhất thực thi mất 1s thì hàm thứ 2 cũng như vậy do thread đang bận thực thi hàm thứ nhất chưa thực thi được hàm thứ 2 nên tổng cộng thực hiện 2 hàm thread sẽ mất 2s

Những bạn có thể thấy kết quả rằng cả 4 hàm gần như đều được thực thi cùng một lúc, điều này chứng tỏ trong quá trình chạy Node-App không chạy đơn luồng, hiểu đơn giản hơn hãy quan sát hình bên dưới.
 

Mọi thứ sẽ phức tạp hơn ảnh trên khá nhiều nhưng nhìn vào đó bạn có thể hiểu rằng thực tế Node-app sẽ rất ít khi chạy đơn luồng mà hầu hết trong quá trình chạy nó sẽ chạy đa luồng.
 
Hãy quan sát hình sau:



Hàm pbkdf2 trên lý thuyết bạn triển khai nó trong code được viết bằng javascript nhưng đằng sau hậu trường tất cả đều là code C++ được thực thi, hàm JS bạn triển khai hiểu đơn giản nó là một lable đại diện cho một hàm khác được viết bằng C++, hàm này được triển khai trong thư việc Libuv và nó  chạy trên một thread hoàn toàn độc lập với EventLoop gọi là thread pool .

Thread pool : là một nhóm gồm 4 thread chuyên dùng để chạy và xử lý các tác vụ chuyên sau liên quan tới CPU, IO, Socket.

=> Như vậy có thể thấy ngoài main thread là nơi mà EventLoop trên bên trên còn một nhóm gồm 4 luồng chạy song song giúp tăng tốc độ xử lý khi gặp nhưng tính toán nặng, không chỉ hàm pbkdf2 được phép sử dụng thread pool mà còn rất nhiều hàm khác cũng có thể sử dụng.

Nhưng có phải bất kì thư viện nào cũng cần triển khai thread pool bên trong nó ?

Bạn hãy thực thi đoạn code sau trên Node:


Bạn có thấy gì đặc biệt ?

=> Dường như tất cả 6 hàm đều cho ra kết quả đồng thời cùng một lúc, khác với triển khai code thực hiện có hàm pbkdf2 phía bên trên, do sử dụng 4 luồng thuộc nhóm thread pool nên nó thực thi kiểu gom 4 hàm cho 1 lần excute.

Hãy quan sát hình sau:


                       
Đổi với đoạn code bên trên hàm request của module http nó cũng chỉ là label đại diện cho một hàm trong thư viện libUV, thứ thực sự được thực thi không phải là code JS bạn tạo ra, nhưng điều khác biệt với hàm pbkdf2 đó là pbkdf2  gọi đến thread pool còn ở hàm request này libuv không thể thực thi các tác vụ mạng hay sâu bên trong hệ thống nên nó sẽ ủy quyền điều này cho HĐH, tất nhiên HĐH cũng sẽ tự quyết định số thread để thực thi tác vụ này (đi sâu vào điều này cực kì phức tạp) và cho chỉ chờ cho HĐH phản hồi lại khi có response trả về.
=> Do HĐH đang thực thi nên nó sẽ không Blocking main thread (Event loop) của chúng ta hay bất cứ thứ gì.

=> Trong Libuv không phải tất cả các hàm đều được triển khai để sử dụng thread pool cũng có những hàm sẽ thực hiện một cuộc gọi ủy quyền đến OS để OS thực thi.

=> Hầu hết các hoạt động liên quan đến Networking, Socket, Listen request, tạo port hầu hết khi triển khai các hàm này trong Node.js , lib sẽ thực hiện cuộc gọi ủy quyền đến OS>

Tổng quan





Nhận xét

Bài đăng phổ biến