Hướng dẫn libuv nodejs - libuv nodejs
Ở bài viết trước ta đã tìm hiểu về cách vận hành của các javascript engine, và một engine tiêu biểu là V8 của Chrome. Tuy nhiên vấn đề là các javascript engine chỉ xuất hiện ở browser, tức là chỉ dịch được ngôn ngữ javascript trên browser. Ta không thể dịch ở các nơi khác, chính vì lẽ đó mà node.js mới ra đời. Node.js giúp chuyển đổi mã javascript ngay trên hệ điều hành mà không cần đến browser, và nó hoạt động ở cả Window, Linux hay Mac. Show Cấu trúcĐể hiểu làm thế nào node.js có thể dịch được Javascript ta phải biết cấu tạo bên trong nó. Các thành phần bao gồm
Như vậy tổng quát là Node.js là nơi có thể thực thi code JS bên ngoài trình duyệt. Và vì được xây bởi V8 nên Node.js có thể tận dụng mọi tính năng của V8. Kế tiếp để hiểu được rõ hoạt động của Node.js ta sẽ không đi chi tiết từng thành phần vì nó vô cùng phức tạp và tốn rất nhiều thời gian, đồng thời không phải ai cũng cần biết tất cả mọi thứ như vậy, nên ta sẽ chỉ hiểu về cơ chế gọi là Asynchronous Event-Driven Nonblocking I/O của Node.jsAsynchronous Event-Driven Nonblocking I/O của Node.js Trước khi ta bắt đầu sẽ đi qua các khái niệm: I/OI/O là quá trình giao tiếp (lấy dữ liệu vào, trả dữ liệu ra) giữa một hệ thống thông tin và môi trường bên ngoài. Với CPU, thậm chí mọi giao tiếp dữ liệu với bên ngoài cấu trúc chip như việc nhập/ xuất dữ liệu với memory (RAM) cũng là tác vụ I/O. Trong kiến trúc máy tính, sự kết hợp giữa CPU và bộ nhớ chính (main memory – RAM) được coi là bộ não của máy tính, mọi thao tác truyền dữ liệu với bộ đôi CPU/Memory, ví dụ đọc ghi dữ liệu từ ổ cứng đều được coi là tác vụ I/O. là quá trình giao tiếp (lấy dữ liệu vào, trả dữ liệu ra) giữa một hệ thống thông tin và môi trường bên ngoài. Với CPU, thậm chí mọi giao tiếp dữ liệu với bên ngoài cấu trúc chip như việc nhập/ xuất dữ liệu với memory (RAM) cũng là tác vụ I/O. Trong kiến trúc máy tính, sự kết hợp giữa CPU và bộ nhớ chính (main memory – RAM) được coi là bộ não của máy tính, mọi thao tác truyền dữ liệu với bộ đôi CPU/Memory, ví dụ đọc ghi dữ liệu từ ổ cứng đều được coi là tác vụ I/O. Do các thành phần bên trong kiến trúc phụ thuộc vào dữ liệu từ các thành phần khác, mà tốc độ giữa các thành phần này là khác nhau, khi một thành phần hoạt động không theo kịp thành phần khác, khiến thành phần khác phải rảnh rỗi vì không có dữ liệu làm việc, thành phần chậm chạp kia trở thành một bottle-neck, kéo lùi hiệu năng của toàn bộ hệ thống. Dựa theo các thành phần của kiến trúc máy tính hiện đại, tốc độ thực hiện tiến trình phụ thuộc:
Do tốc độ I/O thường rất chậm so với các thành phần còn lại, bottle-neck thường xuyên xảy ra ở đây. Người ta thường xét đến I/O Bound và CPU Bound, cố gắng đưa các process bị giới hạn bởi I/O bound về CPU bound để tận dụng tối đa hiệu năng. Blocking I/O vs Nonblocking I/OBlocking I/O Yêu cầu thực thi một IO operation, sau khi hoàn thành thì trả kết quả lại. Pocess/Theard gọi bị block cho đến khi có kết quả trả về hoặc xảy ra ngoại lệ. Nonblocking I/O Yêu cầu thực thi IO operation và trả về ngay lập tức ( Synchronous vs AsynchronousSynchronous: Hiểu đơn giản: Diễn ra theo thứ tự. Một hành động chỉ được bắt đầu khi hành động trước kết thúc. Asynchronous: Không theo thứ tự, các hành động có thể xảy ra đồng thời hoặc chí ít, mặc dù các hành động bắt đầu theo thứ tự nhưng kết thúc thì không. Một hành động có thể bắt đầu (và thậm chí kết thúc) trước khi hành động trước đó hoàn thành. Sau khi gọi hành động A, ta không trông chờ kết quả ngay mà chuyển sang bắt đầu hành động B. Hành động A sẽ hoàn thành vào một thời điểm trong tương lai, khi ấy, ta có thể quay lại xem xét kết quả của A hoặc không. Trong trường hợp quan tâm đến kết quả của A, ta cần một sự kiện Asynchronous Notification thông báo rằng A đã hoàn thành.Asynchronous Notification thông báo rằng A đã hoàn thành. Vì thời điểm xảy ra sự kiện hành động A hoàn thành là không thể xác định, việc bỏ dở công việc đang thực hiện để chuyển sang xem xét và xử lý kết quả của A gây ra sự thay đổi luồng xử lý của chương trình một cách không thể dự đoán. Luồng của chương trình khi ấy không tuần tự nữa mà phụ thuộc vào các sự kiện xảy ra. Mô hình như vậy gọi là Event-Driven.Event-Driven. Event DrivenAsynchronous và Event-Driven không phải là một điều gì quá mới mẻ. Thực tế nó đã tồn tại trong ngành khoa học máy tính từ những ngày đầu. Cơ chế ngắt (Interrupt) là một signal thông báo cho hệ thống biết có một event vừa xảy ra. và Event-Driven không phải là một điều gì quá mới mẻ. Thực tế nó đã tồn tại trong ngành khoa học máy tính từ những ngày đầu. Cơ chế ngắt (Interrupt) là một signal thông báo cho hệ thống biết có một event vừa xảy ra. Khi ngắt xảy ra, hệ thống buộc phải dừng chương trình đang chạy để ưu tiên chạy một chương trình khác gọi là Chương Trình Phục Vụ Ngắt (Interrupt Service Routine) rồi mới quay trở lại thực hiện tiếp chương trình đang chạy dở.Chương Trình Phục Vụ Ngắt (Interrupt Service Routine) rồi mới quay trở lại thực hiện tiếp chương trình đang chạy dở. Có hai loại ngắt: Vậy thành phần nào trong node.js là thứ mang lại cơ chế Asychronous Event-Driven Non-Blocking I/O. Câu trả lời đấy chính là libuv - thư viện multi-platform hỗ trợ asynchronous I/O.libuv - thư viện multi-platform hỗ trợ asynchronous I/O. libuvThông qua hình ta thấy libuv bao gồm các thành phần là: Event Queue, Event Loop và Threal Pool. Nếu như ở các ngôn ngữ như PHP hay Java, để giải quyết vấn đề nhiều yêu cầu cùng một lúc người ta sử dụng khái niệm đa luồng, tức là ứng với mỗi yêu cầu sẽ sinh ra một luồng mới để giải quyết. Nhưng vì node.js sử dụng đơn luồng tức là mọi yêu cầu đều nằm trên một luồng chính (main thread). Nên các yêu cầu sẽ phải nằm trong Event Queue để đợi thực hiên, khi đó Event Loop sẽ tiến hành thực thi các tác vụ trên một luồng duy nhất theo cơ chế non-blocking I/O. Để giảm tải các tác vụ nặng nề, chúng sử dụng thêm Thread Pool. Thread PoolThread Pool là những luồng riêng biệt độc lập với luồng chính ở Event Loop. Trong node.js chúng có thể database, file system, network và vài tác vụ khác. Khi yêu cầu từ Event Queue sang Event Loop, chúng sẽ kiểm tra có phải là tác vụ nặng hay không, nếu phải chúng sẽ được tải xuống Thread Pool và thực thi ở đấy. Điều này giúp luồng chính ở Event Loop giảm nguy cơ bị nghẽn và có thể xử lý các tác vụ đơn giản hơn. Các tác vụ tốn kém phải xử lý ở Thread Pool thường sẽ là :
Notes: Ở đây lưu ý rằng không phải tất cả tác vụ tiêu tốn thời gian đều là tác vụ I/O. Ví dụ như chương trình dưới đây:
Trong đoạn code trên tác vụ chuyên sâu CPU sẽ chặn luồng chính và không thực thi lệnh Event LoopNhư đã đề cập ở trên Event Loop sẽ lấy yêu cầu từ Event Queue để xử lý. Thứ tự thực hiện với các hàm trong Event Loop như sau:
Các thành phần khácV8 EnginePhần V8 đã tìm hiểu ở bài viết trước, giờ đây ta tìm hiểu về ứng dụng của V8 trong nodejs. V8 trong node bao gồm hai phần là call stack và memory heap.call stack và memory heap.
Node APIsChứa các hàm như Callback QueueNơi chứa các hàm ở Node 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. Hoạt độngỞ trên ta đã có các khái niệm về nguyên lý hoạt động và các thành phần trong libuv cùng các thành phần khác. Nhưng chi tiết về quá trình hoạt động giữa các thành phần qua lại với nhau thì sao? Ta có sơ đồ hoạt động như sau:
Tham khảocodehub dattp |