SAM
SAM

Reputation: 1121

Does event loop in Node.js runs callback itself or just passes the callback to call stack in order for call stack to execute the callback?

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?

Upvotes: 3

Views: 969

Answers (1)

jfriend00
jfriend00

Reputation: 707158

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.

  1. setTimeout() runs and schedules a callback to be called for 5 seconds from now.
  2. 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 greeting1 to it so when that callback runs, it will have access to the greeting1 variable. The event loop creates a new call stack, puts its own return address on it and calls the timer callback.
  3. The timer callback runs, that defines greeting2, outputs console.log("A") and runs fs.readFile().
  4. 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.
  5. Then console.log("C") executes. The timer callback then returns back to the event loop.
  6. 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 the fs module (internal to the fs.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 the fs module. When that executes, it then calls your callback and you see the output from console.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:

  1. You call fs.stat().
  2. 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 the stat() operation.
  3. 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.
  4. The thread calls the OS to get the stats for a particular file.
  5. 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.
  6. It then encounters the console.log("A") in the code above and outputs that to the console.
  7. 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.
  8. 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.
  9. That callback runs and executes the console.log(stats).
  10. That callback returns and control goes immediately back to the event loop where it looks for the next event to run.

Upvotes: 5

Related Questions