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

Tác giả: Giang Coffee 

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

Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu 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 sắp đây sở hữu làm nhiều về JS, gặp nhiều lỗi quái đản mình mới tự đặt ra nghi vấn là rốt cục tất cả những thứ trên là dòng gì?, hoạt động thế nào? và vì sao nó mang lại lợi ích?

Hôm nay qua một số google search và đặc thù xem được bà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 san sớt, 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 sở hữu gì góp ý mình vô cùng hoan nghênh và tiếp thu.

Tất cả những tiếng nói lập trình đều được sinh ra để làm thứ tiếng nói giao tiếp giữa người và máy. Dù là tiếng nói 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 những dữ liệu tạm thời ra bộ nhớ, ổ đĩa rồi giao tiếp những 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.Một Stack

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

Nếu trong một hàm (Foo) sở hữu 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 đựng 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. Lúc Bar xong và return thì mới tới lượt Foo được xử lý. Lúc Foo được xử lý xong và return thì Stack rỗng và sẽ đợi những hàm tiếp theo được đẩy vào.

1.2. Heap

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

2. Event Loop là gì

Event Loop là cơ chế giúp Javascript sở hữu 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ói NodeJs sở hữu thể xử lý cả hàng nghìn request cùng một lúc mặc dù nó chỉ tiêu 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 đó, những 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 sở hữu thể xử lý cả nghìn request một lúc với chỉ một thread duy nhất?.

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

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

Event Loop sở hữu tên tương tự bởi vì sở hữu một vòng lặp vô tận trong Javascript Runtime (V8 trong Google Chrome) tiêu dùng để lắng tai những Event.

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

Nhiệm vụ của Event Loop rất thuần tuý đó là đọc Stack và Event Queue. Nếu nhận thấy Stack rỗng nó sẽ nhặt Event trước hết 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ỉ giới hạn lại lúc hàm return hoặc throw exception. Mang tức là trong lúc hàm đang chạy thì sẽ ko sở hữu một hàm khác được chạy, dữ liệu tạm của hàm cũng sẽ ko bị thay đổi bởi một hàm khác hay cũng ko bị giới hạn lại cho tới lúc hoàn thành (ngoại trừ yield trong ES6).

Như những 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 lúc sở hữu 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 ko sở hữu listener thì nó sẽ bị mất và ko đượ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 } });

trước hết phần khai báo biến và hàm sẽ được chạy nhưng ko được đẩy vào stack. Tiếp setTimeout() sẽ được đẩy vào stack và thực hiện. Hàm này ko sở hữu 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 thông số trước hết logInfo (là một callback hoặc sở hữu thể gọi là một event listener cũng được) vào Event Queue. Kế tới sẽ chạy hàm someAsyncOperation và đẩy vào stack, vì hàm này async và sở hữu callback readFileAsync nên readFileAsync được đẩy luôn vào Event Queue mà ko phải chờ như setTimeout để hứng sự kiện đọc xong file (sau 95ms).

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

Quan tâm là Stack LIFO nên someAsyncOperation sẽ nằm dưới cùng còn Event Queue FIFO nên readFileAsync sẽ nằm trên cùng. Sau lúc readFileAsyncđược đẩy vào Event Queue thì someAsyncOperation return và được lấy ra khỏi Stack. Lúc này Stack ko sở hữu gì nên Event Queue sẽ được đọc, nên nhớ là Event Queue chỉ được đọc lúc Stack trống rỗng. readFileAsync sẽ được đẩy vào Event Queue trước vì nó chỉ mất sở hữu 95ms trong lúc logInfo thì phải chờ 100ms. readFileAsync này sẽ được lấy khỏi Event Queue và đẩy vào stack để chạy.

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

readFileAsync sẽ gặp vòng while và giới hạn ở đó 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 lúc readFileAsync thì 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 tới lúc hoàn thành mới thôi nên logInfo ko sở hữu cách nào để giới hạn readFileAsync lại để chiếm quyền điều khiển, trừ lúc trong readFileAsyncsở hữu lệnh yield. Sau 105ms thì readFileAsync return 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. Tương tự logInfo sẽ phải đợi tổng cùng 105ms để được chạy, chứ ko phải 100ms như dự trù. Do đó thông số thứ Hai của setTimeout là thời kì tối thiểu để một Event được đẩy vào Stack và chạy chứ ko phải là thời kì xác thực nó sẽ được chạy.

Giả sử bạn sở hữu 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à yield chỉ nhằm mục đích dễ theo dõi, ta hoàn toàn sở hữu thể bỏ tên hàm đi trong trường hợp này. Lúc Bar và Foo return và được lấy ra khỏi Stack thì yield sẽ được đẩy vào Stack với thông số là DOM Element xảy ra sự kiện click.

Cơ chế run to completion của Javascript sở hữu 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ẽ ko sở hữu hàm nào được chạy nữa, kết quả là Browser sẽ bị đơ, ko phản ứng với những sự kiện như click chuột … Ví dụ :

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

hàm đệ quy ko điểm giới hạn sẽ liên tục đẩy foo vào Stack cho tới lúc đầy, và bạn đoán xem lúc này chúng ta sẽ sở hữu dòng mà hàng ngày những develop đều tìm kiếm Stack 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ì những Browser sẽ throw exception trong trường hợp này :

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

Tới đây ta sở hữu thể thấy cơ chế quản lý theo đầu việc là bí kíp giúp JS Runtime sở hữu thể xử lý hàng nghì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à ủy quyền đám đồ đệ của mình.

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

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

Chào quý độc giả, Em là Tùng Chi, đây là một website chuyên nghiên cứu & biên tập các nội dung chủ đề nấu ăn Chúng tôi sẽ cố gắng hết sức dựa vào kiến thức chuyên môn cũng như khả năng thu thập thông tin từ những nguồn uy tín nhất để cung cấp cho bạn đọc những kiến thức hữu ích nhất mà bạn đọc đang quan tâm.Hy vọng những nội dung Chi cập nhật lên website sẽ giúp được bạn nhiều nhé. Em chúc bạn đọc thật khỏe mạnh, hạnh phúc với gia đình nhỏ tuyệt vời.

Related Posts

Chia sẻ cách làm những con vật bằng giấy dễ thực hiện tại nhà

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…

Ảnh Quỷ Kiếm Dạ Xoa Free Fire Đẹp ❤️125+ Hình Nền Mặt Quỷ FF Ngầu

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…

Tận hưởng trọn vẹn cuối tuần với những lời chúc ngày chủ nhật tốt lành cho người thân yêu

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…

Ảnh Maloch Liên Quân Đẹp ❤️️100+ Avatar, Hình Nền Maloch Chibi

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…

Tuổi Qúy Sửu 1973 xây nhà năm 2022

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…

Mua thịt bò ăn lẩu nên mua những phần nào là ngon nhất? 3+ Cách chọn thịt bò ngon nhất

Tác giả: Giang Coffee  Event Loop là gì và hoạt động thế nào? Trước đây thi thoảng sở hữu làm Javascript và cũng sở hữu nghe nói qua…