Event loops are a paradigm for processing events different than your typical single-threaded or multi-threaded application.
Your request gets broken down into async "events" that are executed in a loop to improve performance and minimize synchronization across threads.
It is famously used by Node.js as the backbone of their event processing and also by several other technologies like Redis and Nginx.
In this article I'll explain the reason for why this paradigm was created and what it tries to optimize.
By the end you'll come out a little wiser, and know more than just "don't block the event loop" :).
Motiviation - why event loops?
To understand why we need event loops we will explore a simple but key example.
Take this straightforward HTTP request code, which sends a request and then tries to read the response from the socket: We do two things in this call - data out and data in.
Both of these actions will end up triggering syscalls through the kernel that write and fetch data.
In terms of time spent on the CPU, this is relatively inexpensive; sending out packets takes very little time, and eventually reading the response will also take very little CPU time.
The key time lost is from waiting on the server to respond to us. will block this thread until the response is available, meaning that the thread cannot be used for any other processing during this time.
If our service is single-threaded, this means that we can't make any requests in parallel and are stuck waiting on any previous requests to finish.
But of course, most services are not single-threaded, so this isn't a huge problem?
Let's continue with the example code, imagining that instead we are processing these requests with multiple threads pulling from a .
Alright now we're cooking!
We have multiple threads processing the list of requests, and we have 10X'ed our throughput, amazing.
But we weren't here to just create yet another multi-threaded setup, so what is the problem here?
Well, one major problem is that we are now managing 10 threads, which all have to be synchronized to get work from .
Synchronization via locks can add a lot of overhead, slowing down our program (so we probably didn't actually get 10x gains here... unlucky).
Another problem is that our throughput has a defined upper bound - 10 requests.
We could solve that by bumping the number of threads.
Let's say I want 10k requests as my throughput - I'll just increase it to 10k and things will be fine, right?
Not quite.
Aside from the increased sync overhead, we also have some additional issues that come up (as the saying goes, there is no such thing as a free lunch..): Extra memory to manage each thread (linear scaling) Coordination with every other thread -> latency (quadratic scaling) More context switches for the CPU.
These are not free and can be a drag on latency at high concurrency.
More threads == more CPU cache line thrashing!
We didn't have all these problems with our single-threaded program, so maybe we can tackle this problem in a different way.
Let's see if we can get back that single-thread magic and start processing in parallel!
Enter the event loop As you might have guessed, our event loop will be single threaded.
So how are we going to avoid the blocking call that we saw earlier?
Two things - a paradigm shift and a bit of help from the kernel.
Paradigm shift: working with Events Now that we can't call directly in our user code, we need to instead return something to express our intent to read as well as a way to continue executing our code.
Let's call that "something" an .
Concretely, an event describes the intent to do a syscall like or and a pointer to the code that should handle the result of that syscall.
Putting that into code, the class would look something like this: Breaking it down, our event class contains: A to say which fd we want to perform the action on An that we intend to execute A that describes what to call with the result of the event for the action to allow passing a
