Event-Driven NodeJS là gì

  • Công nghệ
  • Lập Trình

Hiểu về JavaScript bất đồng bộ Event Loop

Event-Driven NodeJS là gì
Facebook
Linkedin
Telegram

Tác giả: Giang Coffee

Event Loop là gì và hoạt động thếnào?

Trước đây thi thoảng có làm Javascript và cũng có nghe nói qua về một số khái niệm cơ bản và hay ho của Javascript như nhân V8 của Google (quá oách), Event-Driven, Non-blocking I/O, Event Loop những khái niệm giúp JS tận dụng sức mạnh của phần cứng và hàng chục lợi ích khác. Dạo gần đây có làm nhiều về JS, gặp nhiều lỗi quái đản mình mới tự đặt ra câu hỏi là rốt cục tất cả những thứ trên là cái gì?, hoạt động thế nào? và tại sao nó mang lại lợi ích?

Hôm nay qua một số google search và đặc biệt xem đượcbài thuyết trình này mình thấy Event Loop chính là thứ nguồn gốc, hay ho nhất và muốn chia sẻ, thảo luận cùng mọi người. Đấy là những gì mình hiểu ra chứ chưa chắc đã là chuẩn xác. Anh em có gì góp ý mình cực kỳ hoan nghênh và tiếp thu.

Tất cả các ngôn ngữ lập trình đều được sinh ra để làm thứ ngôn ngữ giao tiếp giữa người và máy. Dù là ngôn ngữ gì đi chăng nữa thì cuối cùng vẫn phải dịch ra mã máy, được load lên memory, chạy từng dòng lệnh, ghi các dữ liệu tạm thời ra bộ nhớ, ổ đĩa rồi giao tiếp các thiết bị ngoại vi Thế nên để cho tiện mình xin nhắc lại một số khái niệm cơ bản sau.

1. Một số khái niệm cơbản

1.1 Stack

Stack là một vùng nhớ đặc biệt trên con chip máy tính phục vụ cho quá trình thực thi các dòng lệnh mà cụ thể là các hàm. Hàm chẳng qua là một nhóm các lệnh và chương trình thì gồm một nhóm các hàm phối hợp với nhau. Mỗi khi một hàm được triệu gọi thì nó sẽ được đẩy vào một hàng đợi đặc biệt có tên là stack. Stack là một hàng đợi kiểu LIFO (Last In First Out) nghĩa là vào đầu tiên thì ra sau cùng. Một hàm chỉ được lấy ra khỏi stack khi nó hoàn thành và return.

Event-Driven NodeJS là gì

Nếu trong một hàm (Foo) có triệu gọi một hàm khác (Bar) thì trạng thái hiện tại của hàm Foo được cất giữ trong stack và hàm Bar sẽ được chèn vào stack. Vì đây là hàng đợi LIFO nên Bar sẽ được xử lý trước Foo. Khi Bar xong và return thì mới đến lượt Foo được xử lý. Khi Foo được xử lý xong và return thì Stack rỗng và sẽ đợi các hàm tiếp theo được đẩy vào.

Stack -------------------- | | -------------------- | Bar | <-- -------------------- | Foo | --------------------

1.2. Heap

Heap là vùng nhớ được dùng để chưa kết quả tạm phục vụ cho việc thực thi các hàm trong stack. Heap càng lớn thì khả năng tính toán càng cao. Heap có thể được cấp phát tĩnh hoặc cấp phát động bằng mấy lệnh kiểuallocvớimalloc(đấy là những gì còn nhớ về C++).

2. Event Loop làgì

Event Loop là cơ chế giúp Javascript có thể thực hiện nhiều thao tác cùng một lúc (concurrent model), trước giờ vẫn nghe nóiNodeJscó thể xử lý cả hàng ngàn request cùng một lúc mặc dù nó chỉ dùng một thread duy nhất (Single Threaded). Nếu như ở PHP hay Java thì với mỗi một request sẽ sinh ra một thread để xử lý request đó, các thread hoạt động độc lập, được cấp bộ nhớ, giao tiếp ngoại vi và trả về kết quả. Vậy làm thế nào để NodeJs có thể xử lý cả ngàn request một lúc với chỉ một thread duy nhất?.

Có một sự thật là trên web browser thì trong khi get data từ các url thì người dùng vẫn có thể thực hiện các thao tác khác như click button và gõ vào các ô textbox. Tất cả là nhờ có các web apis và cơ chế hoạt động của Event Loop. Tuy Js Runtime chỉ có một thread duy nhất nhưng các web apis giúp nó giao tiếp với thế giới multi thread bên ngoài, tận dụng các con chip đa nhân vốn rất phổ biến hiện nay. Web apis giúp đẩy các job ra bên ngoài và chỉ tạo ra các sự kiện kèm theo các handler gắn với các sự kiện. Kể cả đối với NodeJs khi không có web apis thì nó vẫn có các cơ chế tương đương khác giúp đẩy job ra bên ngoài và chỉ quản lý các đầu việc. Web Apis hoạt động như vậy thì Event Loop sẽ thế nào?

3. Event Loop hoạt động như thế nào?

Event Loop có tên như vậy bởi vì có một vòng lặp vô tận trong Javascript Runtime (V8 trong Google Chrome) dùng để lắng nghe các Event.

while (queue.waitForMessage()) { queue.processNextMessage(); }

Event-Driven NodeJS là gì

Nhiệm vụ của Event Loop rất đơn giản đó là đọc Stack và Event Queue. Nếu nhận thấy Stack rỗng nó sẽ nhặt Event đầu tiên trong Event Queue và handler (callback hoặc listener) gắn với Event đó và đẩy vào Stack. Đặc điểm của việc thực thi hàm trong JS là sẽ chỉ dừng lại khi hàm return hoặc throw exception. Có nghĩa là trong khi hàm đang chạy thì sẽ không có một hàm khác được chạy, dữ liệu tạm của hàm cũng sẽ không bị thay đổi bởi một hàm khác hay cũng không bị dừng lại cho đến khi hoàn thành (ngoại trừyieldtrong ES6).

Như các bạn thấy trên hình thì JS Runtime còn thao tác với một callback queue hay event queue ngoài stack ra. Event queue này khác với stack ở chỗ nó là queue kiểu FIFO (First In First Out). Mỗi khi có một Event được tạo ra, ví dụ user click vào một Button thì một Event sẽ được đẩy vào Event queue cùng với một handler (event listener) gắn với nó. Nếu một Event không có listener thì nó sẽ bị mất và không được đẩy vào Event queue. Để cho dễ hình dung cách thức hoạt động của Event Loop ta lấy một ví dụ như sau:

const fs = require('fs'); function someAsyncOperation(callback) { // giả sử đọc file hết 95ms fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(function logInfo() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // đọc file xong sẽ tiếp tục chờ thêm 10ms someAsyncOperation(function readFileAsync() => { const startCallback = Date.now(); // chờ 10ms while (Date.now() - startCallback < 10) { // do nothing } });

đầu tiên phần khai báo biến và hàm sẽ được chạy nhưng không được đẩy vào stack. TiếpsetTimeout()sẽ được đẩy vào stack và thực hiện. Hàm này không có trong Javascript Runtime mà là hàm tiện ích của Browser, nó sẽ khởi tạo một bộ đếm và sau đúng 100ms thì nó sẽ đẩy tham số đầu tiênlogInfo(là một callback hoặc có thể gọi là một event listener cũng được) vào Event Queue. Kế đến sẽ chạy hàmsomeAsyncOperationvà đẩy vào stack, vì hàm này async và có callbackreadFileAsyncnênreadFileAsyncđược đẩy luôn vào Event Queue mà không phải chờ nhưsetTimeoutđể hứng sự kiện đọc xong file (sau 95ms).

Stack Event Queue -------------------- ------------------- | | | readFileAsync | <-- -------------------- ------------------- | | | | -------------------- ------------------- | someAsyncOperation | <-- | | -------------------- -------------------

Để ý là Stack LIFO nênsomeAsyncOperationsẽ nằm dưới cùng còn Event Queue FIFO nênreadFileAsyncsẽ nằm trên cùng. Sau khireadFileAsyncđược đẩy vào Event Queue thìsomeAsyncOperationreturn và được lấy ra khỏi Stack. Lúc này Stack không có gì nênEvent Queuesẽ được đọc, nên nhớ làEvent Queuechỉ được đọc khi Stack trống rỗng.readFileAsyncsẽ được đẩy vàoEvent Queuetrước vì nó chỉ mất có 95ms trong khilogInfothì phải chờ 100ms.readFileAsyncnày sẽ được lấy khỏi Event Queue và đẩy vào stack để chạy.

Stack Event Queue -------------------- ------------------- | | ------ | readFileAsync | -------------------- | ------------------- | | | | logInfo | <-- -------------------- | ------------------- | readFileAsync | <-- | | -------------------- -------------------

readFileAsyncsẽ gặp vòngwhilevà dừng ở đó 10ms. Vậy tổng cộng hàm đọc file sẽ mất 105ms để hoàn thành. Nhưng ở giây thứ 100 thìlogInfođược đẩy vào Event Queue (lúc này đã rỗng) trong khireadFileAsyncthì còn phải mất thêm 5ms nữa mới hoàn thành. Vì cơ chế của Javascript là chạy đến khi hoàn thành mới thôi nênlogInfokhông có cách nào để dừngreadFileAsynclại để chiếm quyền điều khiển, trừ khi trongreadFileAsynccó lệnhyield. Sau 105ms thìreadFileAsyncreturn và được lấy ra khỏi Stack.

Stack Event Queue -------------------- ------------------- | | ------ | logInfo | -------------------- | ------------------- | | | | | -------------------- | ------------------- | logInfo | <-- | | -------------------- -------------------

Một lần nữa Stack lại trống vàlogInfođược đẩy vào Stack. Như vậylogInfosẽ phải đợi tổng cộng 105ms để được chạy, chứ không phải 100ms như dự tính. Do đó tham số thứ 2 củasetTimeoutlà thời gian tối thiểu để một Event được đẩy vào Stack và chạy chứ không phải là thời gian chính xác nó sẽ được chạy.

Giả sử bạn có một đoạn code jQuery như sau:

$('#button_1').click(function yield() { console.log('Ouch!'); });

thì một hoặc vài event sẽ được đẩy vào Event Queue như sau:

Stack Event Queue -------------------- ------------------- | | | yield(Event) | <-- -------------------- ------------------- | Bar | | | -------------------- ------------------- | Foo | <-- | | -------------------- -------------------

đặt tên hàm làyieldchỉ nhằm mục đích dễ theo dõi, ta hoàn toàn có thể bỏ tên hàm đi trong trường hợp này. Khi Bar và Foo return và được lấy ra khỏi Stack thì yield sẽ được đẩy vào Stack với tham số là DOM Element xảy ra sự kiện click.

Cơ chếrun to completioncủa Javascript có một điểm bất lợi đó là nếu một hàm chạy quá lâu hoặc bị vòng lặp vô tận thì sẽ không có hàm nào được chạy nữa, kết quả là Browser sẽ bị đơ, không phản ứng với các sự kiện như click chuột Ví dụ:

function foo() { console.log('i am foo!'); foo(); } foo();

hàm đệ quy không điểm dừng sẽ liên tục đẩyfoovào Stack cho đến khi đầy, và bạn đoán xem lúc này chúng ta sẽ có cái mà hàng ngày các develop đều tìm kiếmStack Overflow

Stack Event Queue -------------------- ------------------- | foo | | Event 1 | -------------------- ------------------- | foo | | Event 2 | -------------------- ------------------- | foo | | Event 3 | -------------------- -------------------

Để tránh tình trạng Browser bị treo vì lỗi lập trình thì các Browser sẽ throw exception trong trường hợp này:

MAXIMUM CALL STACK SIZE EXCEEDED.

Hầu hết các thao tác trong Javascript đều là bất đồng bộ nhưng có một số ngoại lệ thú vị như hàmalert(hàm này là của Browser API, không có trong NodeJs). Khi hàm này được chạy thì bạn không thể thực hiện một thao tác nào khác ngoài click OK.

Đến đây ta có thể thấy cơ chế quản lý theo đầu việc là bí kíp giúp JS Runtime có thể xử lý hàng ngàn tác vụ cùng một lúc. Giống như bạn được giao một đống việc, bạn chia nhỏ từng việc và giao cho đám đệ tử của mình.

Bài viết gốc được đăng tải tạiGiang Coffee

Tuyển lập trình viên Javascript lương cao tại đây

  • TAGS
  • event loop
  • javascript
  • queue
  • sync
Facebook
Linkedin
Telegram
Event-Driven NodeJS là gì
Ban Biên Tập Blog TopDev. Nice to meet you