Tìm hiểu về Event Loop và các Timers, process.nextTick trong NodeJS



  • Chào các bạn, sau quá trình tìm hiểu về Event Loop. Mình vẫn chưa hiểu rõ được cách hoạt động của các timers và process nextTick trong event loop như thế nào. Cụ thể mình có vài khúc mắc như sau:

    Như trong official document của Node.js (https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) có đoạn viết như vầy:
    "When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop."

    Tức là khi mình chạy 1 ứng dụng NodeJs bằng lệnh node app.js, việc đầu tiên nó sẽ làm là initialize event loop, sau đó check các dòng code javascript được viết và thực thi các dòng lệnh đó, từng dòng 1 sẽ được đẩy vào call stack. Tại đây call stack sẽ thực thi từng dòng lệnh và đẩy sang Node JS API các hàm cần Node JS API thực thi (như là setTimeout, setImmidiate, processNextTick, v.v...).

    Khi các dòng code đã đc thực thi hết, tức là call stack trống, event Loop sẽ start. Theo thứ tự như sau:

    ┌───────────────────────┐
    ┌─>│ timers │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ I/O callbacks │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ idle, prepare │
    │ └──────────┬────────────┘ ┌───────────────┐
    │ ┌──────────┴────────────┐ │ incoming: │
    │ │ poll │<─────┤ connections, │
    │ └──────────┬────────────┘ │ data, etc. │
    │ ┌──────────┴────────────┐ └───────────────┘
    │ │ check │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    └──┤ close callbacks │
    └───────────────────────┘

    (reference: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)
    Và nó cũng có nói là:
    "You may have noticed that process.nextTick() was not displayed in the diagram, even though it's a part of the asynchronous API. This is because process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation completes, regardless of the current phase of the event loop."

    Tức là process.nextTick callback sẽ được thực thi sau khi hoạt động hiện tại hoàn tất bất kể đang ở phase nào của event loop. Vậy vấn đề ở đây là hoạt động hiện tại (current operation) là hoạt động gì? Nó là call stack lần đầu tiên hoàn tất hay là hoàn tất 1 phase trong event loop hay là gì đó?

    Giả sử mình có 1 đoạn code như sau:
    setTimeout(function() {console.log('Timeout called');}, 0);
    process.nextTick(function(){console.log('nextTick ');});

    Kết quả luôn luôn sẽ là:

    nextTick
    Timeout called

    Như vậy callback của process.nextTick luôn luôn được thực thi trước. Trong khi đó theo như process diagram trên thì event loop sẽ thực thi các event trong timers queue trước?

    Mình có 2 giả thuyết như sau:

    1. Vì event loop start cùng lúc với node process (ở 1 thread khác) nên lúc này ở timers phase chưa có gì hết và event loop sẽ chạy thẳng đến poll phase rồi đứng đây chờ. Khi call stack chạy hết các dòng code javascript (tức là setTimeout đã được thực thi và callback của nó đã nằm trong timers phase), Node Js sẽ chuyển qua kiểm tra event loop (lúc này đang ở poll phase) => poll phase trống nhưng lại có 1 timers đang chờ nên nó chuyển sang timers phase. Trong quá trình này, từ poll phase chuyển sang timers phase tức là hoàn tất poll phase => process nextTick được gọi. Và kết quả là process nextTick được thực hiện trước.

    2. Hoàn tất call stack được coi như 1 operation complete => process.nextTick được gọi.

    Và mình cũng không chắc là 2 giả thuyết trên của mình có cái nào đúng không, các bạn có ai biết về vụ này help mình với. Xin cám ơn nhiều :D.



  • câu trả lời trong Document ghi rất rõ : process.nextTick() không phải một phần của event loop. Callback của nextTick thực thi ngay khi hoạt động trong function (gọi nextTick) kết thúc. Ví dụ của bạn:

    function test(){
     setTimeout(function B() {console.log('Timeout called');}, 0);
     process.nextTick(function A(){console.log('nextTick ');});
    }
    test();
    

    Ở đây đơn giản khi setTimeout gọi -> Test() kết thúc -> A() được gọi -> B() được gọi.

    một ví dụ khác để thấy mức độ ưu tiên nextTick và setImmediate, setTimeout ;

    var log = console.log;
    
    function test() {
    
    	var i = setInterval(function() {
    		log('.');
    	}, 100);
    
    	setTimeout(function timeout0() {
    		console.log('timeout0');
    	});
    
    	setImmediate(function immediate() {
    		log('Immediate');
    	});
    
    	process.nextTick(function A() {
    		log('A');
    		process.nextTick(function B() {
    			log('B');
    			process.nextTick(function C() {
    				log('C');
    			});
    			process.nextTick(function D() {
    				log('D');
    			});
    		});
    
    		process.nextTick(function E() {
    			log('E');
    			process.nextTick(function F() {
    				log('F');
    			});
    			process.nextTick(function G() {
    				log('G');
    			});
    		});
    	});
    
    	setTimeout(function timeout1000() {
    		console.log('timeout1000');
    		clearInterval(i);
    	}, 1000)
    }
    
    test();
    // A B E C D F G timeout0 Immediate ....  timeout1000
    
    

    Vậy:

    • Nó là call stack lần đầu tiên hoàn tất hay là hoàn tất 1 phase trong event loop hay là gì đó? - nextTick là nextTickQueue. Nó sẽ thực thi ngay khi call stack trống, trước cả các event loop.
    • Trong khi đó theo như process diagram trên thì event loop sẽ thực thi các event trong timers queue trước? - như phần đầu đã nói, nextTick là một phần riêng # event loop nhưng chung mô hình Asynchronous API của nodejs


  • @hidemanvn
    Cám ơn bạn vì câu trả lời trên, như vậy theo bạn tức là khi 1 operation complete tức là call stack trống thì nextTickQueue sẽ được thực thi. Tuy nhiên, nếu vậy thì mình lại gặp vấn đề với đoạn code với đây:

    //setTimeout 1
    setTimeout(function A (){
    console.log('timeout 1 called!!!');
    process.nextTick(function E (){
    console.log('test 5');
    })
    });
    //setTimeout 2
    setTimeout(function B (){
    console.log('timeout 2 called!!!');
    });
    //setTimeout 3
    setTimeout(function C (){
    console.log('timeout 3 called!!!');
    });
    //setTimeout 4
    setTimeout(function D (){
    console.log('timeout 4 called!!!');
    });

    và kết quả là:
    timeout 1 called!!!
    timeout 2 called!!!
    timeout 3 called!!!
    timeout 4 called!!!
    test 5

    tuy nhiên nếu chạy lại vài lần, kết quả có thể là:
    timeout 1 called!!!
    test 5
    timeout 2 called!!!
    timeout 3 called!!!
    timeout 4 called!!!

    và tiếp tục chạy thêm vài lần nữa, kết quả cũng có thể là:
    timeout 1 called!!!
    timeout 2 called!!!
    test 5
    timeout 3 called!!!
    timeout 4 called!!!

    Chúng ta có thể thấy callback của process nextTick đôi lúc thực thi ngay sau khi callback function A của setTimeout 1 kết thúc, nhưng cũng có khi thực thi ngay sau khi callback function D của setTimeout 4 kết thúc. Nếu như định nghĩa "1 operation complete tức là call stack trống thì nextTickQueue sẽ được thực thi", thì processNexttick phải luôn luôn được thực thi ngay sau khi callback function của setTimeout 1 kết thúc, đồng nghĩa với việc callback function E của process.nextTick phải luôn luôn xuất hiện ngay sau setTimeout 1. Tuy nhiên ta có thể thấy process nextTick callback function lại có thể nằm sau setTimeout 4 callback function D, suy ra không phải processNextTick được thực thi ngay sau khi call stack trống.

    Cụ thể mình sẽ chạy thử Node JS process cho đoạn code trên để dễ hình dung:

    1. run command node test.js để chạy

    2. NodeJs process initialize event loop và check javascript source code (JSSC).

    3. Dòng đầu tiên của JSSC được đẩy vào call stack: setTimeout 1 được thực thi, đẩy sang Node JS API xử lý => tại đây Node JS API sẽ chạy các hàm timer và đẩy callback function A vô timers queue trong timers phase => Kết thúc dòng đầu tiên. (timer queue: [ A() ]; nextTickQueue: [ ])

    4. Dòng thứ 2 của JSSC được đẩy vào call stack: setTimeout 2 được thực thi, đẩy sang NodeJS API xử lý => tại đây Node JS API sẽ chạy các hàm timer và đẩy callback function B vô timers queue trong timers phase. => Kết thúc dòng thứ 2 (timer queue: [ A(), B() ]; nextTickQueue: [ ]).

    5. setTimeout 3 thưc thi => kết thúc (timer queue: [ A(), B(), C()]; nextTickQueue: [ ]).

    6. setTimeout 4 thực thi => kết thúc (timer queue: [ A(), B(), C(), D()]; nextTickQueue: [ ]).

    7. Hết script => call stack trống, Node JS kiểm tra event loop => timer queue không rỗng => thực thi các event trong timer queue.

    8. A() đươc đẩy vô call stack, thực thi function A().

    9. console.log('timeout 1 called!!!'); được đẩy vô call stack và thực thi => 'timeout 1 called!!!' được xuất ra màn hình.

    10. process.nextTick(function E (){console.log('test 5');}) được đẩy vô call stack và thực thi => function E được đẩy vô nextTickQueue.

    11. Kết thúc function A() => call stack trống tiếp tục kiểm tra event loop.

    => ngay tại đây như bạn đã thấy, call stack trống sau khi A() thực thi xong, nếu theo như giả thuyết "1 operation complete tức là call stack trống thì nextTickQueue sẽ được thực thi" thì nextTickQueue phải được gọi ngay lúc này. Nhưng như kết quả thực tế, đôi lúc function E lại được gọi ngay sau timeout 4, cũng có thể gọi ngay sau timeout 2 ... Tức là nextTickQueue không phải lúc nào cũng được thực thi ngay sau khi call stack trống.

    Bạn có ý kiến gì về vấn đề này không? Mong nhận được ý kiến của bạn. Cám ơn bạn nhiều. :D.

    P/S: Bạn có thể chỉ mình cách quote đoạn code trong khung như bạn được ko? Mình thưc sự ko biết cách làm như vậy luôn nên hơi khó nhìn, thông cảm nhé :D.



  • Khung cmt bạn có thể gõ kiểu Markdown. Markdown-Cheatsheet

    Về code ví dụ của bạn, bạn đã đặt nextTick vào một timer. Mình cần bạn đọc kỹ lại phần cảnh báo bôi đen tại đây để thấy nguy cơ giữa event loop và nextTick. Đó cũng là lí do mình ít khi dùng setTimeout và nextTick đi cùng, mà dùng thư viện Async của Caolan để kiểm soát sự không đồng bộ.

    Còn về phần giải thích của bạn. Tại số 8, hàm Athực thi xong là callback E của nó được gọi rồi. Vấn đề là hàm B,C,D lúc này cũng đã và đang thực thi, hàm nào xong trước thì in ra trước thôi, chẳng cớ gì E chạy trước thì - hoàn thành trước. Nên vị trí kết quả test 5 mới vậy. Xem thêm Process.nextTick changes để thấy tại sao E luôn được gọi ngay sau khi A thực thi xong >


Log in to reply