Event Loop - Behind the Scene

Event Loop - Behind the Scene

·

3 min read

I talked about the how the event loop works in a previous article. Recently I learned some concepts about its implementation. Let's share it in this article.

First, let's take a look at below code. It is just a simple for loop. Inside the loop, the current page is fetched and the loop index will be pushed if the fetch succeed.

const result = [];
for (let i = 0; i < 10; i++) {
    fetch("./").then(() => result.push(i));
}

If we run it in browser, and then check out the result value after like 10 seconds, we may see a result like below.

result
// [5, 0, 8, 6, 4, 1, 7, 9, 2, 3]

Just as we expected, because the fetch call is asynchronous, the next request is sent without needing to wait for the first request's return, so we get a result value unsorted like above.

The question is, why this fetch call is asynchronous? How to implement this asynchronous function call?

To answer this question, we need to go deeper. We know that, applications run upon operating systems. Networking, file system accessing, all these things go down to the operating system calls.

So now the question becomes, is this http request function call asynchronous at the system level?

The answer is no. At the system level, this http request function call is synchronous. That's why in other languages like Python or Java, a http request is synchronous too.

import requests

r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

# below code runs only after the above get request returns
r.status_code
# 200

So how can we make this synchronous function call asynchronous? The answer is, it depends on the system level api too. Basically, there are 2 solutions.

First, if there is some asynchronous technique we can use from the system api, then we use them. For example, for io operations in Linux, there is a system api called epoll. Inside the api, we could use the function epoll_wait to watch for file descriptors. This makes the io operations asynchronous.

Second, if there is no such technique we can use from the system api, we could just use a new thread to run this task. The pseudo code is like below.

// create a new thread to run this task
t = new Thread(request);
t.on("end", () => {
    // when the thread ends, execute this callback
});

// main thread maintains the event loop

As you can see, because the synchronous code is run in another thread, so it will not block the main thread, so we can see it as asynchronous.

In Node.js, these two solutions are both being used to implement this event loop logic. That's why we get these asynchronous apis for free.

Below image I took from this video, which shows which apis rely on the system calls, which rely on the threads.

Screen Shot 2022-10-28 at 10.38.10.png

I have not dig into the real implementation code in C/C++, because I am not familiar with it. But I think, as a js developer, understanding these concepts behind the scene is pretty important. By the way, I learned a lot from the above video, I recommend watching it.