Exploring the Event Loop
Introduction
Ikram Saedi introduces the talk "It Feels Parallel But It's Actually Sequential: Exploring the Event Loop," which aims to explain the concept of parallelism in JavaScript and answer whether JavaScript is truly parallel.
The Illusion of Parallelism in JavaScript
Ikram discusses why JavaScript feels parallel, using examples like website interactions, animations, and background processes happening seemingly simultaneously. She contrasts this with the frustrating experience of a non-parallel system where users constantly face loading screens.
Parallelism vs. Concurrency
Ikram uses the analogy of cooking with a friend to illustrate the difference between parallelism (two chefs managing two pots simultaneously) and concurrency (one chef switching between managing two pots). She highlights how concurrency uses fewer resources while achieving similar results.
Parallelism in Computing
Moving from the cooking analogy to computing, Ikram explains that true parallelism involves running processes on separate CPU cores at the same time. She points out that computers can handle many more processes than the number of cores, hinting at the role of concurrency.
Threads: Streams of Work
Ikram introduces the concept of threads as lightweight, non-isolated processes that share resources within a process. She contrasts this with processes that have their own dedicated memory space. She promises this concept will be relevant later in the talk.
Threads vs. Processes in Chrome
Ikram uses the example of Chrome's tab isolation to further differentiate threads and processes. She explains how isolating tabs as separate processes prevents one unresponsive tab from crashing the entire browser window, unlike threads which could bring down the whole window.
JavaScript: A Single-Threaded Language
Ikram states that JavaScript is single-threaded, meaning it has only one stream of work. This implies that, in theory, each action in JavaScript should block the next, leading to a poor user experience.
Non-Blocking JavaScript
Ikram demonstrates how JavaScript achieves a non-blocking nature despite being single-threaded. She uses the example of uploading a video on Twitter while still being able to interact with the user interface, highlighting the role of concurrency.
The Event Loop: Managing Asynchronous Operations
Ikram introduces the event loop as the mechanism behind JavaScript's non-blocking behavior. She explains that the event loop manages the flow of function calls between the call stack and the event queue.
Understanding the Call Stack
Ikram explains the call stack as a LIFO (Last-In, First-Out) data structure, analogous to a stack of books. She describes how function calls are pushed onto the stack and popped off when they return, using a code example to illustrate the process.
The Event Queue and Web APIs
Ikram describes the event queue as a FIFO (First-In, First-Out) queue where events from Web APIs are placed. She explains that Web APIs are built-in browser functionalities like DOM manipulation, console logging, and network requests.
How the Event Loop Works: A Tweet Example
Ikram revisits the Twitter tweet example to illustrate the interaction between the call stack, event queue, and Web APIs. She walks through the steps of uploading a video, typing a tweet, and adding an emoji, showing how the event loop prevents blocking.
The Event Loop's Simple Logic
Ikram summarizes the event loop's function as continuously checking the call stack and pushing the first function from the event queue onto the stack when it's empty, highlighting its continuous and iterative nature.
Interacting with the Event Loop: Asynchronous Operations
Ikram emphasizes that asynchronous operations like setTimeout, Promises, and async/await are key to understanding the event loop's impact. These operations demonstrate how JavaScript can handle tasks without blocking the main thread.
Illustrating the Event Loop: A Cat Animation
Ikram presents a visual simulation of the event loop using a cat animation, demonstrating how the event loop processes tasks, interacts with Web APIs, and ensures smooth execution of asynchronous operations.
Challenges and Solutions in Animation Implementation
Ikram discusses challenges she encountered while creating the animation, specifically with queuing animations correctly. She explains how she used Promises and async/await to ensure animations happened sequentially, creating a smoother visual flow.
Alternative Approaches to Concurrency: Multithreading
Ikram acknowledges that the event loop is not the only way to achieve concurrency. She introduces multithreading, where multiple threads within a process run in parallel, as an alternative approach used in other programming languages.
Comparing Multithreading and the Event Loop
Ikram compares multithreading and the event loop, highlighting their respective strengths and weaknesses. She notes that both are suitable for I/O-bound tasks (waiting for external input/output), but multithreading can be advantageous for CPU-intensive operations.
Debunking the Myth of Parallelism in Multithreading
Ikram clarifies that multithreading, while appearing parallel from a programmer's perspective, isn't truly parallel as it doesn't necessarily use multiple CPU cores. The illusion of parallelism comes from the rapid switching between threads managed by the computer.
JavaScript's True Nature: Sequential Execution
Ikram concludes by affirming that JavaScript, despite its asynchronous capabilities, is fundamentally sequential. She reiterates that tasks happen one after the other, though the event loop and asynchronous operations create the illusion of parallelism.
Closing and Contact Information
Ikram shares her contact information, including her Twitter handle, GitHub profile, and a link to the event loop simulation. She thanks the audience for their time and attention.
Okay, cool.
Thank you so much for introducing me.
This is the talk of It Feels Parallel But It's Actually Sequential Exploring the Event Loop by me.
This talk will delve into what parallelism really means and answer the question as to whether or not JavaScript is actually parallel.
This talk requires some basic JavaScript knowledge but whether or not you are experienced or new to programming, I hope this talk will be useful to you.
Let's get started.
Cool.
Why does JavaScript feel parallel?
As a user interacting with a website, there's just so much happening all at once.
You can be mousing over a bunch of different things, clicking on things, copying and pasting, typing in a bunch of stuff.
The possibilities are endless.
As you're doing all of this, the components on your page are constantly re rendering, they're animating, there may be a bunch of fetch requests happening in the background.
And in the meanwhile, you're not necessarily just continually waiting for a loading screen.
When would it not feel parallel?
If you are constantly watching the spinning wheel of death, as I'm sure many of you are familiar with, this is a really, sucky experience.
This is when things would not feel like they ever would.
It would not feel like a bunch of things are happening simultaneously, you're just waiting and waiting endlessly.
What does parallelism actually mean?
I like to think about it as a cooking night with a friend.
You have two people managing and cooking each pot of food.
I really like cooking like a rice and a curry at the same time.
If I have a friend over, let's say they're taking care of the rice pot and I'm taking care of the curry pot.
Here we have a diagram.
For each pot, we have a Red rectangle and we also have beige rectangles.
The red rectangles are just the amount of time that you are like actively spending on cooking each pot and the beige is just where you're like standing back and not actually doing anything.
For example, my friend could be washing and boiling the rice, doing nothing, simmering the rice, doing nothing, and then they'll turn the rice off and your rice is done.
Me, on the other hand, I have a lot more work to do.
I have to put in my onions and garlic, I can chill for a little bit, but then I've got to put in my spices, my tomatoes, my other vegetables, and I'll keep on going until my dish is done.
But a way, more likely, situation for me is concurrency.
So instead of me having a friend over, it'll just be me cooking dinner just for myself.
So instead, I will be managing both the rice pot and the curry pot.
Cool.
Instead it will look something more like this.
I first am actively managing the rice pot.
So I've got, I'm like washing and boiling my rice.
Now I don't need to do anything as it boils, I'm handing off that task back to the burner.
Then I'm switching back to my, I'm switching over to my curry pot.
And now I can like, saute my onions and my garlic.
But hey, the burner has let me know, has let me know that the rice is finished boiling, I can turn it down, I can turn it down to a simmer.
I can switch back to my curry, I can do a bunch of stuff, I can add in my spices, tomatoes and vegetables, then go back to my rice, et cetera, et cetera.
I'm constantly switching back, And fourth, and in both scenarios you still end up with a rice and a curry, except with concurrency you're using less resources and with parallelism you're using more.
What is parallelism in computing?
So away from the chefs.
So when two processes, it's when two processes are running on two separate cores in the CPU simultaneously.
So at exactly the same time, eight processes can be run on an eight core MacBook Air.
So you can think about each core like a chef, and each process like a pot.
However, you can do many, more things than just eight things at once on an eight core MacBook Air.
If you're like me, I can have a hundred different tabs open all at once.
It's way, way, more than eight.
So this is just a screenshot of Activity Monitor, on the MacBook, where you can see all of the different processes that are running.
Right now, and clearly, we have 409.
That is a lot more than 8.
So how is this actually working?
So your computer is actually switching between processes like millions and millions of times a second.
So at exactly, at the exact nanosecond or millisecond or whatever, you will have actually 8 processes running, but within the span of a second, your computer may have switched between 409.
And then you may have also noticed something sitting right above it, which is threads.
So what is that?
Basically, you can have many, threads associated with a process.
So as you can see, we have 3, 289 threads here.
And they're basically a stream of work on a process.
They're essentially, In a process, they are essentially a process, but like a light version of one.
They don't get their own space and memory, and because they're not isolated, threads can interfere with each other.
I promise this will be relevant later in the talk.
This is not a tangent.
Thread versus processes, Chrome is famous for tab isolation.
It implements this by creating a process for each tab.
If one tab becomes fully unresponsive, then the entire Chrome window will not necessarily have to fully crash.
But, with threads, it would probably take down your whole window, which would suck.
So how does JavaScript fit into all of this?
It is a single threaded language, using only a single process, so there is only one stream of work, and in theory, every action would have to block the next one.
And what would that mean for us?
Here you can see my really cute cat, Loaf, who I'm going to be referring to constantly throughout this talk.
He is the most important piece of information that you can get out of this talk, so if you get anything, please remember his name.
But let's say you want to go post a tweet onto Twitter to talk about your cat, and you want to upload a cute video of him.
If JavaScript was a blocking language and you first decided to upload a video of your cat and then you were like, oh, I actually want to write some text, you would have to wait for the full 20 seconds or however long it takes for your video to upload before you're able to interact with the rest of the page.
So it looks something like this.
These are arbitrary, arbitrary measurements, but, for example, it could take, 20 seconds for the, post request to the Twitter upload's endpoint, to, give you a successful response.
And then only after that are you able to do a bunch of typing and add in some cute emojis, which would really suck.
For example, you've pressed the upload button, and now you're just watching the spinning wheel of death for 20 seconds, you'd be counting down, 1 1000, 2 1000, 3 1000, and it's taking forever, right?
I'm not gonna wait.
But then, now you're finally able to type in your stuff, and now you're finally able to add in your cute little cat emoji.
But, JavaScript is non blocking, so you can upload your cute cat video to the world, but then also still interact with the rest of Twitter's UI in the meantime.
Here I'm able to upload my cute cat video, and I only really have to wait, actually like a millisecond or so, and then I'm able to type in my hello cutest cat in the world, I'm able to add in my emoji, and all is well.
It is a concurrent single threaded language and it can multitask just like me cooking.
Why does it work this way?
The reason why it works this way is because of the event loop.
And what is the event loop?
The event loop basically manages like the flow of function calls between primarily the call stack and the event queue.
So let's take a look at what each of these data structures actually mean.
And let's start off with the call stack.
What is the call stack?
The call stack is a last in, first out data structure.
I personally find it really helpful to think about it like a stack of books.
The last book you put on the top of your book stack is the first one you can pick up without causing the entire pile to fall apart.
And the same applies to functions in JavaScript.
But instead of books, we have frames, and frames correspond with a function that has been called, and a function is pushed onto the stack when it has been called, and is removed or popped off the stack when it returns.
So to go through just a really simple example, we have a function called first.
First just returns a, double of a number, so for example, one.
Then, we, in order to double it, it will have to push the double frame onto the call stack, and then double will be calling multiply, which is another function, which will also now, wrong function name, but that's okay, pretend the top one is multiply, and, yeah, that will also need to be pushed onto the call stack.
Now we've successfully doubled our number, multiply now has returned, it's given you two, then it will be popped off the call stack.
Then double has returned as well, it has popped off the call stack, and first has now returned as well, and your call stack is now empty and clear.
Now, what is the event queue?
The event queue is just like a queue of people, it's diversity and first served.
And the, events get pushed to the event queue from a web API of some sort.
And then what is the web API?
This is just a broad umbrella term for a bunch of different web APIs that come embedded in JavaScript's browser engine.
So common examples include the HTML DOM API, the console API, the URL API, the fetch API, I didn't realize that all of these were kind of APIs that were actually sitting in your browser.
I never really thought about them until I researched this.
So I thought that was really interesting.
And back to what the event loop actually is.
Let's run through an example.
Going back to my example of creating a tweet about my cute cat, Loaf.
Let's say we have a function called createLoafTweet.
It will post and upload to Twitter, then it will, once that upload has finished uploading, it will console.log the loaf video has been uploaded, then we can type in our tweet, and then we can also add our emoji.
So what would this look like?
First we call the createLoafTweet function, this will be pushed onto the call stack.
Then we call the postTwitterUpload function, which is then pushed onto the call stack.
This will then get shifted off for the fetch API to handle, because it's making a fetch request to the twitterUploadsAPI endpoint, and it's ticking over in the meantime, but it's no longer on the stack.
It's been popped off the call stack.
Now we can type in our tweet, which is the next function call, and that is happening whilst the upload has been uploading in the Twitter API.
Then that's been removed off the call stack.
Then we add in our cute little cat emoji at the end.
And then, we've pushed that, popped that off the call stack, and then now, the entire createLoafTweet function has been popped off the call stack.
However, our Twitter upload is still sitting in the API.
And we can wait around for 20 seconds, but I'm not going to do that.
Let's pretend it's finished executing, and then it will, so the little arrow function here is what I'm trying to refer to when I'm using the dot then in the Fetch API.
All right.
This will then get pushed onto the event queue, and then the event loop is like, hey, there's nothing in the call stack, so let's push in our then, callback into the call stack, and then we can use our console log for loafVideoUploaded, and then we're done.
We've pushed off the loaf video has been uploaded, and then we've pushed off the then call back as well.
What is the event loop actually doing?
It's actually very, simple.
All it's really doing is constantly checking that the call stack is empty.
If it is, it will push the first function call from the event queue into the call stack.
How you interact with it.
Anything asynchronous, and I feel like that is when you start really tangibly interacting with the event loop.
Set timeouts, promises, async awaits, are all things that easily display the true nature of the event loop.
Cute cat intermission.
I have a cute little simulation, of the event loop using some really cute cats.
Let's see if I can get this guy up, please.
Cool.
Okay.
We have our cute little event loop.
Here's my little cat in the corner.
His name is Loaf.
He is the event loaf now.
We're going to click the center button.
This will order us a loaf of bread.
Click this guy, and he will pick up our loaf order and chuck it onto the call stack.
Now we'll need to prepare it and send it off to the kitchen.
And our lovely Chef Web API.
We'll be cooking it in the meantime.
Now, he's finished, we've finished preparing, and we've finished the order loaf, but we haven't fully served the loaf yet.
Our Chef API has delivered it to the event queue, And then, eventually, our little server event loaf will pick it up from the event queue, and then we'll drop it into the call stack for it to be executed, and then ta da!
Our loaf has been served.
And that is the event of the simulation.
Thank you!
[applause] Okay.
I ran into some issues when actually creating this animation.
I did a bit of an oopsies when I first made it.
I was not properly queuing my animations one after the other, and I totally failed at it.
If we take a look, close look at this function, we have just a function called move.
It is like a kind of contrived example from my actual code that I used.
So I have some keyframes just telling my loaf where to go, and just telling him to transform and translate to different coordinates on, the page.
Then I've got some animation options.
All they're really doing is just saying, hey, this animation lasts for three seconds.
And let's zoom in on these two.
Here in my first one, I'm saying, hey, can you please move to the center button to pick up the order loaf.
And then after that, I want him to move to the call stack to drop it in.
However, the second animation does not wait for the first animation to finish.
And it looks a little bit funky.
As we can see, I've clicked my autoloaf and oh, he didn't magically move over to the autoloaf.
He just appeared and like just teleported there and looks really, weird.
So if you play that again, just magically appears in the center of the screen.
No smooth transition at all.
So the way in which I fix this, is adding in the dot finished at the end of my cat dot animate block.
So the finished will return a promise.
And now I'm saying hey.
Only after this first animation has finished, will we then go on to the next animation.
And we're essentially like saying, hey, JavaScript, please wait for this first animation to finish.
A nice cleaner way to do it is using async await as well.
Same thing, but just cleaner syntax.
Hey, please wait for the first animation to finish.
Then please wait again for our second animation to finish.
And when using Promises, every line of code that is awaited or then is put into the event queue rather than being immediately executed in the call stack, which is great.
You can actually ensure that your animations are queued one after the other.
How do other languages implement concurrent multitasking?
Because JavaScript is not the only language in the world.
There are so many others that implement , that have to implement something similar.
Multithreading is an alternative, right?
This is when, within a process, multiple threads are being executed in parallel.
For example, something someone would do would be to use one thread to handle incoming requests, another thread to handle a bunch of CPU intensive work.
And both multi threading and the event loop are good for, I O work, which is basically when you're waiting for an input and, sorry, waiting for, the result of an API request or something, and then switching over.
And handling things.
Okay, cool.
When is multithreading better than the event loop?
Imagine you've implemented a search field where on every key press you're iterating over a large amount of data to find the search result.
This leads to a very laggy experience when typing because the function invocations stay primarily in the call stack and they're not handed off to a web API or anything like that.
And you can end up with a really laggy typing experience.
Something like this, where you just have things just jumping on the page, it's not smooth, it's not, real time typing.
However, in theory, multi threading, the CPU processing can happen without slowing down the UI, and you're able to just type freely.
However, the event loop does have a really big leg up over multi threading.
It's way simpler and it's a lot more predictable, it's a lot easier to debug, you will not necessarily have threads that are randomly switching in the middle of executing a function, and you know exactly how your functions are going to be executed.
Is multi threading parallel?
Not in the true sense.
I actually did think it was parallel when I first looked into it.
But it is not because it does not require multiple CPU cores.
To you as a programmer it may as well be the computer kind of handles the switching between different threads for you at the CPU level.
But it is not truly parallel.
Is JavaScript sequential?
Yes.
No two things are actually happening at exactly the same time in JavaScript.
It is always one after the other and this is far simpler than true parallelism as well as multi threading.
As I was introduced earlier, I'm a software engineer at an edtech company called Stile Education.
I have my Twitter handle up here and my GitHub handle, as well as a link to my event load simulation, which is also just on my GitHub.
Everything is just my first and last name.
So I'm very easy to find and yeah, thank you so much for listening.