Introduction: From Operating Systems Lectures to Practice
Back in school when I was studying operating systems, concepts like concurrency and parallelism were presented as essential tools for Operating Systems to be able to effectively and efficiently schedule tasks. These concept were at times abstract, discussed in the context of processes, threads, and CPU scheduling. Around the same time, I was building an e-commerce web application using Next.js (essentially React + an API Ecosystem), which required using JavaScript's
async/await
pattern to manage asynchronous tasks (eg. fetching data from multiple different endpoints within a single web page in a way that was fast and responsive to the user).As I continued to learn about both of these topics in parallel, the connections between them quickly began to form in my head - which, after pursued a bit, brought deeper levels of understanding in both.
In this post, I want to guide you through the journey I took—from understanding concurrency and parallelism in theory to applying them to in modern web development using JavaScript's
async/await
mechanic. We’ll start from the basics, hopefully :) explain each concept clearly, and provide code examples to illustrate what these things look like in practice.Part 1: Concurrency and Parallelism in Operating Systems
Before diving into JavaScript, let's start with the basics of concurrency and parallelism, to ensure we're all on a similar footing going into this.
1.1 What is Concurrency?
- Definition: Concurrency is about managing multiple tasks at the same time but not necessarily executing them simultaneously. In operating systems, this is often achieved through techniques like time slicing (where a single CPU core switches between 2 or more tasks) or context switching (where the state of a process is saved so it can be resumed later).
- Example: Imagine you're in a coffee shop. The barista takes an order (Task 1), starts brewing the coffee (Task 2), and while the coffee is brewing, takes another order (Task 3). The barista is managing multiple tasks concurrently, even though they’re only actively working on one task at a time. This represents a process being executed concurrently, on a single CPU-core (represented by a single Barista performing the tasks here).
- Code Analogy (Pseudocode):
function takeOrder() { console.log("Taking order..."); } function brewCoffee() { console.log("Brewing coffee..."); } function serveCoffee() { console.log("Serving coffee..."); } // Simulating concurrency takeOrder(); brewCoffee(); // This task takes time takeOrder(); // The barista takes another order while coffee is brewing serveCoffee();
1.2 What is Parallelism?
- Definition: Parallelism involves executing multiple tasks simultaneously, usually on different processors or cores. This is possible when the system has multiple CPUs or a multi-core CPU.
- Example: Imagine two baristas working side by side. Each barista represents a CPU core on a multi-core CPU. One is taking orders while the other is brewing coffee at the same time. Both tasks are happening simultaneously, which speeds up service.
- Code Analogy (Pseudocode):
function barista1() { console.log("Barista 1: Taking order..."); console.log("Barista 1: Serving coffee..."); } function barista2() { console.log("Barista 2: Brewing coffee..."); } // Simulating parallelism barista1(); barista2(); // Both baristas work at the same time, representing processes running on different CPU cores
Part 2: Transitioning from OS Theory to JavaScript
Now that we have a feel for the basics of concurrency and parallelism in the context of operating system process scheduling, let's see how these concepts translate into JavaScript, specifically in modern web development.
2.1 The Single-threaded Nature of JavaScript
- Explanation: JavaScript is single-threaded, meaning it can only execute one piece of code at a time. This might sound limiting, that was definitely my first thought, but JavaScript uses a clever mechanism called the event loop to manage multiple tasks efficiently without blocking the main thread. If you are new to JS, I highly recommend you get intimately familiar with it. It is extremely important across all JS-based frameworks/runtimes (React, Angular, Next.js, Node.js, you name it...).
- Event Loop: The event loop allows JavaScript to handle tasks like fetching data from a server, waiting for user input, or processing timers. It does this by queuing tasks and executing them one at a time when the main thread is available.
- Code Example:
console.log("Start"); setTimeout(() => { console.log("This runs after 2 seconds"); }, 2000); console.log("End");
setTimeout
function delays the execution of its callback, the event loop ensures that "End" is logged before the delayed message because the main thread isn't blocked.2.2 The Evolution of Asynchronous Programming in JavaScript
- Callbacks: Originally, JavaScript handled asynchronous operations with callbacks. A callback is a function passed to another function, which is then executed after the asynchronous operation completes.
- Callback Hell: As applications grew more complex, deeply nested callbacks led to messy and hard-to-read code, a problem known as "callback hell."
- Code Example of Callback Hell:
getUserData(userId, (user) => { getPosts(user.id, (posts) => { getComments(posts[0].id, (comments) => { console.log(comments); }); }); });
- Promises: To improve this, JavaScript introduced promises, which represent a value that will be available in the future. Promises allow for chaining operations and better error handling.
- Async/Await:
async/await
was introduced to further simplify asynchronous code. It allows developers to write asynchronous operations in a way that looks synchronous, making the code easier to read and understand.
Part 3: Deep Dive into Async/Await with Practical Examples
Now that we understand the history and evolution of asynchronous programming in JavaScript, let's dive into
async/await
and see how it implements the concepts of concurrency and parallelism.3.1 Understanding Async/Await through Concurrency
- Async Functions: An
async
function is a function that always returns a promise. You use theawait
keyword inside this function to pause its execution until the promise is resolved.
- Concurrency with Async/Await: Just like the time slicing in an OS,
async/await
allows JavaScript to handle multiple tasks by pausing one operation (await
) and continuing with others until the first is ready to proceed.
- Code Example:
async function fetchData() { const userData = await fetchUserData(); const details = await fetchUserDetails(userData.id); console.log(details); } fetchData();
fetchData
function pauses at each await
until the asynchronous operation completes. This allows JavaScript to manage multiple tasks efficiently, even though it’s single-threaded.3.2 Applying Parallelism with JavaScript
- Promise.all for Parallelism: While JavaScript is single-threaded, you can achieve a form of parallelism by starting multiple promises at the same time with
Promise.all
. This method allows you to execute multiple promises simultaneously and wait for all of them to complete.
- Code Example:
async function fetchMultipleData() { const [userData, posts] = await Promise.all([fetchUserData(), fetchPosts()]); console.log(userData, posts); } fetchMultipleData();
fetchUserData
and fetchPosts
start simultaneously. The function waits for both operations to complete before moving on, similar to how an OS would handle multiple threads in parallel.3.3 Real-World Applications Across Frameworks
- React and useEffect: In React, managing side effects like data fetching often involves using
useEffect
withasync/await
to handle asynchronous tasks within a component. This allows your component to perform actions like fetching data from an API without blocking the UI.
- Code Example:
useEffect(() => { const fetchData = async () => { const data = await fetchSomeData(); setData(data); }; fetchData(); }, []);
- Next.js Data Fetching: Next.js enhances data fetching with methods like
getServerSideProps
, which often useasync/await
to handle server-side data before rendering a page. This is crucial for server-side rendering and ensuring that data is ready before the page is delivered to the user.
- Code Example:
export async function getServerSideProps() { const data = await fetchDataFromAPI(); return { props: { data } }; }
getServerSideProps
is executed on the server side. It fetches data before the page is rendered, ensuring that the data is available when the page reaches the client.Part 4: Putting It All Together - Async/Await as a Concurrency Tool
4.1 Async/Await in the Event Loop
- Recap: Just as the OS uses processes and threads to manage tasks concurrently, JavaScript uses the event loop with
async/await
to manage asynchronous operations. This ensures that long-running tasks don’t block the main thread, keeping the UI responsive.
- Code Example:
async function handleRequest() { const userData = await fetchUserData(); console.log("User data fetched"); const posts = await fetchPosts(userData.id); console.log("Posts fetched"); return { userData, posts }; } handleRequest().then(data => console.log(data));
handleRequest
fetches user data, waits for it to complete, then fetches posts based on the user ID. All of this happens without blocking other operations.4.2 When Not to Use Async/Await
- Performance Considerations: While
async/await
simplifies code, it’s not always the best choice for highly parallel tasks. In cases where you need true parallelism (e.g., CPU-intensive tasks), using tools like Web Workers might be more efficient.
- Code Example with Web Workers:
// worker.js self.onmessage = function(event) { const result = heavyComputation(event.data); self.postMessage(result); } // main.js const worker = new Worker('worker.js'); worker.postMessage(data); worker.onmessage = function(event) { console.log("Result from worker:", event.data); }
Conclusion: From Concepts to Code
I hope this little writeup provided you with at least some value! My goal was to approach Asynchronous Web Programming from a non-traditional direction by extending academic understandings in concurrency/parallelism to modern asynchronous web development.
Whether you’re just starting out or you’re an experienced developer, I highly suggest taking the time to get comfortable with these concepts as they will make you much more effective and aware when using them across your software development ventures, front or back-end:
- JavaScript’s
Event Loop
- Concurrency / Parallellism
- Asynchronous vs Synchronous Web Programming
For newer developers: If you're interested in any sort of web development that includes JavaScript/TypeScript, I higgghhllyyy recommend getting familiar with JavaScript's
event loop
. In my opinion a deep understanding of it will be wildly powerful throughout your early (and beyond) careers. Below I have 2 videos which I cannot recommend enough which were massively helpful for me: