Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

[single thread] đã nói lên vấn đề, nhưng trong vài cuộc tranh luận gần đây nhiều member do nhầm lẫn hoặc gây nhầm lẫn cho người khác bằng những cụm từ như chạy ngầm, xử lý song song.

Cơ bản điểm khác nhau của JS và các ngôn ngữ khác nằm ở callback, tức nếu yêu cầu một tác vụ nào đó thường liên quan đến I/O như truy xuất DB, đọc file, gọi ajax . . . nó sẽ ra yêu cầu và việc xử lý có thể do process bên ngoài đảm nhận (như Mongodb, mysql, broswer . . . ) và khi nào có kết quả thì nó trả về qua callback.

Khả năng này làm nodejs trong như nhanh hơn các ngôn ngữ khác, có khả năng xử lý song song . . .

Còn làm multi-thread không phải cứ code là tự nó multi-thread, các ngôn ngữ support tính năng này như Java/C# phải khai báo kiểu đối tượng, cài đặt tiến trình, kiểm xoát tiến trình . . . nên thường coding thì điều single thread như nhau thôi !

VD PHP có support xử lý multi-thread nhưng thậm chí nó còn không được khuyến khích xử dụng. Khi chạy web các tác vụ thường phải thực hiện tuần tự nên gần như không có chổ cho multi-thread.

Với mình thì hiểu thế nào cũng OK, quan trong nắm được bản chất của nó !

Còn làm multi-thread không phải cứ code là tự nó multi-thread, các ngôn ngữ support tính năng này như Java/C# phải khai báo kiểu đối tượng, cài đặt tiến trình, kiểm xoát tiến trình . . . nên thường coding thì điều single thread như nhau thôi !

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

Có chạy được không?

Chúng ta vẫn thường nghe thấy javascript là single thread, dart là single thread phải không nhỉ? Nhưng liệu nó có đúng single thread, nghĩa là cả chương trình của chúng ta chỉ có 1 thread duy nhất đang chạy mà thôi? Chúng ta hãy cùng đi giải mã bí ẩn này nhé.

Thử chạy Node.js

Chúng ta hãy tạo ra file index.js với 1 dòng duy nhất:

while(true);

Để đảm bảo chúng ta không tạo ra bất kì thread nào ngoài main thread. Sau khi chạy chương trình trên MacOS, chúng ta sẽ count số thread với câu lệnh:

NUM=`ps M <pid> | wc -l` && echo number of thread is: $((NUM-1))

Trên máy của mình sẽ nhận được kết quả thế này:

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

Nghĩ là đang có 7 thread đang tồn tại trong chương trình của chúng ta.

Thử chạy với dart

Chúng cũng tạo ra một app có file hello_world.dart với 1 dòng duy nhất:

while(true);

Cũng là để đảm bảo chúng ta không tạo ra bất kì thread nào ngoài main thread. Và khi chạy lệnh count số thread mình nhận được kết quả:

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

Nghĩ là đang có 4 thread đang tồn tại trong chương trình của chúng ta.

Giải mã bí ẩn

Qua 2 ví dụ với các framework, chúng ta thể thấy thực sự không có đơn luồng nào ở đây cả. Nó có thể chỉ có 1 luồng duy nhất để xử lý, nhưng sẽ có rất nhiều luồng để cung cấp sự kiện và hỗ trợ cho nó.

Nhiều thành phần tác động

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

Một chương trình sẽ chịu tác động từ rất nhiều thành phần khác nhau. Các thành phần này có thể là:

  1. Mạng internet (hay socket): cung cấp dữ liệu cho chương trình.
  2. Từ các thiết bị ngoại vi: điều khiển chương trình.
  3. Trình dọn rác: giúp chúng ta tự động giải phóng bộ nhớ.
  4. Các trình lập lịch của hệ điều hành.

Đối với các chương trình chạy trên server thì nó chịu tác động chủ yếu từ socket để xử lý và phản hồi lại thông tin cho người dùng.

Một luồng duy nhất

Để rõ ràng hơn, chúng ta hãy nhìn vào chương trình chỉ có 1 luồng duy nhất này nhé:

public class SingleThread { public static void main(String[] args) { NonBlockingEventLoop eventLoop = new NonBlockingEventLoop(3000); eventLoop.onUpdate(() -> { // các event có thể được thêm ở đây. eventLoop.addEvent(() -> System.out.println("Hello World")); }); eventLoop.start(); // vòng lặp đã chạy, nên chúng ta sẽ không thể thêm event ở đây } }

Rõ ràng là chúng ta sẽ chỉ có hàm onUpdate để thêm được các event và queue, như trong ví dụ chúng ta chỉ có mỗi việc in ra 1 dòng "Hello World". Câu hỏi đặt ra là làm thế nào để phát sinh ra được các sự kiện? Nếu không có sự kiện nào thì chương trình sẽ hoạt động 1 cách vô nghĩa.

Cơ chế chuyển luồng

Vậy câu hỏi đặt ra là với nhiều luồng như vậy thì làm sao đưa hết dữ liệu về main thread được?

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

Vậy với nhiều thành phần tác động, cũng tương đương với nhiều luồng tác động như vậy, thì làm sao chúng ta có thể chuyển hết sự kiện về 1 main thread duy nhất được? Câu trả lời đó là sự kết hợp giữa các queue và các event loop.

Các luồng I/O sẽ phát sinh ra các sự kiện cho main queue, main thread sẽ xử lý các sự kiện này và nó cũng tạo ra các sự kiện khác nhau cho các queue khác nhau. Các thread tương ứng với các queue này sẽ xử lý các event đó và tạo ra một vòng tuần hoàn cho chương trình của chúng.

Ưu điểm

Đơn luồng có một ưu thế cực lớn khi nó:

  • Giúp chúng ta không phải lo lắng về lock, về synchronize, về race condition cũng như deadlock. Đặc biệt với các ứng dụng client, nơi chúng ta cần phải render đồ hoạ và đảm bảo tính nhất quán cao, tránh cho chương trình bị crash và tăng trải nghiệm của người dùng.
  • Rất dễ dàng để tạo ra các chương trình với chỉ một vài dòng lệnh.

Nhược điểm

Nhưng được cái này thì lại mất cái kia. Không phải làm quen với synchronized hay lock thì chúng ta lại phải làm quen với async và await. Và đôi khi đơn luồng chính là cái bẫy khiến cho toàn bộ hệ thống của chúng ta bị treo, và đây không phải trường hợp hiếm gặp. Ví dụ với chương trình này:

function runWitPromise() { return new Promise(resolve => { while(true); }); } async function runAsync() { runWitPromise(); } runAsync(); console.log("Finished");

Vì hàm callback của promise sẽ gọi trên main thread nên while(true) sẽ làm block toàn bộ chương trình của chúng ta nên Finished sẽ không được in ra. Hậu quả là trong những chương trình xử lý phức tạp, khả năng phục vụ cho toàn bộ user sẽ bị chậm trễ, một hàm xử lý nặng nề nào đó sẽ làm cho toàn bộ hệ thống bị ảnh hưởng.

Tổng kết

Qua các thử nghiệm chúng ta có thể thấy rõ ràng với 1 thread duy nhất sẽ rất khó để tạo ra các chương trình lớn, phục vụ các bài toán phức tạp. Có chăng đơn luồng chỉ phục vụ cho các bài toán nhúng kiểu như đèn nhấp nháy hay các thiết bị tương đối đơn giản.

Cái mà chúng ta vẫn gọi là đơn luồng trên các framework như node.js, dart hay các redis, thực chất chỉ là "1 luồng xử lý duy nhất", chứ xung quanh nó vẫn tồn tại rất nhiều các luồng khác để làm I/O và GC.

Làm việc với đơn luồng sẽ giúp chúng ta tránh phải học các kiến thức đau đầu với đa luồng, tuy nhiên khi sử cụng await phải rất cẩn thận để tránh làm block cả chương trình, hãy ưu tiên sử dụng callback hơn nếu có thể nhé.

teeeeeeeee said:

Dùng cái setTimeOut là là đã sử dụng webAPI r mai fen
Cứ cho một sự kiện click r vòng lặp 1000000 r log ra kq là đc r mà


via Tác Giả for iPhone

hình như click event nó cũng là webapi

Tác giả: Giang Coffee 

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 đượ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 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ại sao js single thread mà lại xử lý nhiều event cùng lúc được

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.

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.

Tại sao js single thread mà lại xử lý nhiều event cùng lúc được

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.

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ểu alloc với malloc (đấy là những gì còn nhớ về C++).

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ói NodeJs có 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 ?

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(); }

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ừ yield trong 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ếp setTimeout() 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ên logInfo (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àm someAsyncOperation và đẩy vào stack, vì hàm này async và có callback readFileAsync nên readFileAsync đượ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ê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 khi readFileAsyncđược đẩy vào Event Queue thì someAsyncOperation return và được lấy ra khỏi Stack. Lúc này Stack không có gì nên Event Queue sẽ được đọc, nên nhớ là Event Queue chỉ được đọc khi Stack trống rỗng. readFileAsync sẽ được đẩy vào Event Queue trước vì nó chỉ mất có 95ms trong khi 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à 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 khi 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 đến khi hoàn thành mới thôi nên logInfo không có cách nào để dừng readFileAsync lại để chiếm quyền điều khiển, trừ khi trong readFileAsynccó 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. Như vậy logInfo sẽ 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ủa setTimeout là 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à yield chỉ 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 completion củ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 đẩy foo và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ế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ì 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àm alert (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ại Giang Coffee

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

  • TAGS
  • event loop
  • javascript
  • queue
  • sync