I was learning Node.js and came across event loop and how it handles asynchronous tasks in Node.js. As far as I understood now and I might be wrong)), when, for example, we use asynchronous, say, fs.readFile() that readFile will be taken out of main thread and will be passed to OS kernel(so that kernel handles readFile while JS engine can keep reading the rest of the code in main thread). Then, Once kernel is done with readFile, event is emitted which will be taken by event loop. So, the question is after event was accepted by event loop, will event loop execute BY ITSELF the callback or will event loop takes that callback form queue and passes the callback into call stack in order for the call stack to execute that callback?
-
Watch this video youtube.com/watch?v=8aGhZQkoFbQ He has explained Event loop in detail.chandan_kr_jha– chandan_kr_jha2020-04-27 03:35:04 +00:00Commented Apr 27, 2020 at 3:35
-
@chandan_kr_jha, certainly I watched it, he says that event loop serves as a middleman that takes callback from queue and put that into call stack. Ok, but some articles on how event loop in Node.js says that event loop itself runs the callback which confused me))SAM– SAM2020-04-27 03:39:32 +00:00Commented Apr 27, 2020 at 3:39
-
this will clear your doubts. nodejs.org/en/docs/guides/event-loop-timers-and-nexttickchandan_kr_jha– chandan_kr_jha2020-04-27 03:41:16 +00:00Commented Apr 27, 2020 at 3:41
1 Answer
A callback coming from the event loop starts a from-scratch, new and empty call stack. Whatever call stack it was in at the time has long since finished and returned control back to the event loop.
The callback will start with a lexical context record which gives that callback access to the appropriate lexical environment (local variables, etc... that were in scope when the callback was defined), and that is reattached to the callback before it is executed.
And, executing the callback will be the initiation of a new call stack that supports the callback itself calling other functions and makes it so when the callback returns, control will go back to the event loop.
FYI, a call stack is just a record of where to return back to when a function returns and it is referred to as a stack because it can build up and then unwind as a() calls b() which then calls c() which then returns and goes back to b() and then returns and goes back to a(). That's the stack part. The call stack is just the storage mechanism for all this.
Since a callback called from the event loop just returns back to the event loop when it's done, the only thing on the call stack at the moment the callback is initiated will be the return address back to the event loop. So, when the callback returns, control comes back to the event loop.
So, the question is after event was accepted by event loop, will event loop execute BY ITSELF the callback or will event loop takes that callback form queue and passes the callback into call stack in order for the call stack to execute that callback?
This sounds like some possible terminology confusion. A call stack is just a stored set of return addresses. The JS interpreter uses the call stack to remember return addresses and then to jump back to return addresses when a function returns. It's the interpreter running the show, not the call stack running the show. The call stack is just data.
So, let's say you had this code:
let greeting1 = "Hello";
console.log("AA");
setTimeout(() => {
let greeting2 = "GoodBye";
console.log("A");
fs.readfile('./myfile.txt', () => {
console.log("B", greeting1)
});
console.log("C");
}, 5000);
console.log("BB");
Here's the sequence of events with that code.
setTimeout()runs and schedules a callback to be called for 5 seconds from now.- 5 seconds later when the JS interpreter is back to the event loop, it sees the timer is ready to fire. It calls the callback for that timeout and attaches the appropriate lexical record that contains
greeting1to it so when that callback runs, it will have access to thegreeting1variable. The event loop creates a new call stack, puts its own return address on it and calls the timer callback. - The timer callback runs, that defines
greeting2, outputsconsole.log("A")and runsfs.readFile(). fs.readFile()initiates an asynchronous operation to read that file and then returns. As part of that, it registers a completion callback to be called.- Then
console.log("C")executes. The timer callback then returns back to the event loop. - Some time later, the
fs.readFile()operation (which actually consists of multiple separate asynchronous operations, but I've simplified here to just one) completes when the file is finally closed. That file closing triggers a callback to get called in thefsmodule (internal to thefs.readFile()code).
The event loop then similarly creates a new callstack, puts its own return address on it, attaches the appropriate lexical record to it and calls the callback internal to thefsmodule. When that executes, it then calls your callback and you see the output fromconsole.log("B", greeting).
The console output from this would look like:
AA
BB
A
C
B, Hello
Sorry, some questions again, firstly, is it really true that there is OS kernel that handles asynchronous readFile() and once readFile is done, passes the emitted event to event loop? Secondly, so after event loop gets emitted event concerning the completion of readFile(), event loop just calls the callback. But as you can see, all hard was done by OS kernel and event loop just executes the easy part which is callback. So, is it true that even callback can be blocking thus thread pool is used if callback is blocking being hard for event loop?
Let's not talk about readFile() for a moment, because that's a multi-stage operation of opening the file, doing one or more reads and then closing the file and that's a mix of Javascript, native C++ code in node.js and OS library calls. So, that's a bit more complicated. Let's pick something similar, but simpler. Let's pick a single operation like fs.stat() with this code:
fs.stat('./myfile.txt', (stats) => {
console.log(stats);
});
console.log("A");
Here are the steps:
- You call
fs.stat(). - This goes into node.js Javascript code for the
stat()call. That call sets up some context to allow Javascript data to be passed to C++ code and then calls a C++ function (still nodejs-specific code, but now in C++ code) for thestat()operation. - The C++ function then prepares for an OS call to get the stats for a particular file. But, rather than execute that call in the current thread which would block the Javascript engine while it is running, it uses a native C++ thread (obtained from a native code thread pool) and tells that thread to go run the OS call.
- The thread calls the OS to get the stats for a particular file.
- Meanwhile, back in the first C++ function that received the original stat call, after it launched the thread to go carry out the OS call, it returns back to Javascript. The Javascript
stat()operation gets returned back to from the C++ and it returns back to your Javascript where it continues executing other Javascript. - It then encounters the
console.log("A")in the code above and outputs that to the console. - Meanwhile, the OS is working away at getting the stats from the file. When it gets that result, it returns that result back to the thread from the thread pool. That thread then posts an event to the nodejs event loop. That event contains not only the results of the stat, but also the context needed for the completion callback in your Javascript code.
- When the event loop gets free from doing other things and this event gets its turn, the event loop then creates the right Javascript context (that was originally passed in with the original
fs.stat()call, sets that up and calls the Javascript callback associated with that completion event. - That callback runs and executes the
console.log(stats). - That callback returns and control goes immediately back to the event loop where it looks for the next event to run.