Mastering JavaScript Threads: How It Handles Multiple Operations & Users
JavaScript is known for being single-threaded, but what does that mean? And how can it manage multiple tasks and serve multiple users at once? To understand this, let's start with the basics.
What is a Thread?
A thread is like a line of workers. In computing, a thread is the smallest sequence of programmed instructions that can be managed by the operating system. When we say JavaScript is single-threaded, it means there’s only one worker handling all tasks, one at a time.
This may seem limiting, especially when you consider how a modern app often needs to handle multiple tasks at once, like loading images, handling user input, and fetching data from a server. But JavaScript has ways to handle this using asynchronous programming!
Single-Threaded: The Basics
JavaScript is single-threaded by nature. This means it can only do one thing at a time. Imagine having a worker in a factory who can only work on one task before moving on to the next. This is how JavaScript operates — it has one thread that processes code, called the main thread.
Call Stack:
The call stack is a data structure that keeps track of where the program is in its execution. When you call a function, it gets pushed onto the stack, and when the function returns a result, it's popped off the stack. Here's an example:
function first() {
console.log('First function');
}
function second() {
console.log('Second function');
}
first();
second();
In this code, the first() function is added to the stack, executed, then removed, followed by the second() function.
Synchronous vs. Asynchronous
By default, JavaScript executes code synchronously, meaning one thing after another. But what happens when a task takes time, like fetching data from a server? We don’t want to wait and block other tasks, so JavaScript uses asynchronous programming.
Synchronous:
In synchronous programming, tasks are executed one after the other, waiting for each task to complete before moving on to the next. This approach works fine for simple tasks, but it becomes inefficient for time-consuming operations, like reading from a file or fetching data from a server.
console.log('Task 1');
console.log('Task 2');
Executes only after Task 1 finishes
Let's see another example:
console.log('Start');
for (let i = 0; i < 1000000; i++) { /* long loop */}
console.log('End');
In the example above, the code runs line by line, blocking further execution until the current task (the loop) is completed. This means users would have to wait for the loop to finish before anything else can happen.
Asynchronous:
In asynchronous programming, tasks are started but can continue running in the background. This way, JavaScript can keep working on other tasks while waiting for the asynchronous operation to complete. When the background task is done, JavaScript picks up where it left off.
Example:
console.log('Start');
setTimeout(() => {
console.log('Task Complete');
}, 2000);
console.log('End');
Output is in this manner :
Start
End
Task Complete
Now, the question is, why is 'End' from the console executed before the 'Task Complete'?
Here, JavaScript does not block the entire program while waiting for the setTimeout function to finish. It moves on and finishes the other tasks (console.log('End')), and once the timer is done, it logs the message "Task Complete". This is the key advantage of asynchronous programming — tasks can run in the background without halting the flow of the main thread.
JavaScript uses callbacks, promises, and async/await to manage asynchronous tasks.
Callback:
One of the earliest techniques JavaScript used for handling asynchronous tasks was callbacks. A callback is simply a function passed into another function as an argument, to be executed later when the time is right.
领英推荐
function fetchData(callback) {
setTimeout(() => {
callback('Data fetched!');
}, 2000);
}
fetchData((message) => {
console.log(message); // This will log "Data fetched!" after 2 seconds
});
This will log "Data fetched!" after 2 seconds
Here, fetchData uses a setTimeout to simulate a long task. The callback function (in this case, a function that logs the message) is passed in and gets called after 2 seconds when the task is done.
However, callbacks have a problem: callback hell.
Callback Hell:
When callbacks are nested inside other callbacks, the code can become hard to read and maintain:
doTask1(() => {
doTask2(() => {
doTask3(() => {
doTask4(() => {
console.log('All tasks completed');
});
});
});
});
This is often referred to as "callback hell" because it creates deep nesting and makes the code messy and difficult to follow. Thankfully, JavaScript introduced a better way to handle asynchronous code: promises.
Promise:
A more modern way to handle asynchronous tasks. Promises allow better error handling and chaining. A promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises allow us to write asynchronous code more cleanly, without the need for deeply nested callbacks.
let fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched successfully!');
}, 2000);
});
fetchData
.then((message) => {
console.log(message); // Logs "Data fetched successfully!" after 2 seconds
})
.catch((error) => {
console.error(error);
});
Promise States:
Now, Let's see how we can write the 'promise' in a cleaner way :
Async/Await: The Modern Solution
To make promises even easier to work with, JavaScript introduced async and await. This allows us to write asynchronous code that looks and behaves like synchronous code, without the hassle of chaining .then() calls.
async function fetchData() {
try {
let result = await new Promise((resolve) => {
setTimeout(() => resolve('Data fetched successfully!'), 2000);
});
console.log(result); // Logs "Data fetched successfully!" after 2 seconds
} catch (error) {
console.error(error);
}
}
fetchData();
By using await, the function pauses execution until the promise resolves, making the code easier to read and maintain, as if it were synchronous.
Now, we know about the call stack and callback. It's time to learn about Event Loop!
Event Loop: How JavaScript Manages Async Code
The event loop allows JavaScript to execute synchronous code, manage asynchronous operations, and continue processing without blocking the main thread.The call stack executes all synchronous code.
Here’s how it works:
This makes JavaScript non-blocking and allows it to handle multiple operations efficiently, even though it’s single-threaded.
How Does JavaScript Handle Multiple Requests?
When JavaScript is used on the server (e.g., Node.js), you might wonder how it handles requests from multiple users if it’s single-threaded.
Node.js uses an event-driven, non-blocking I/O model. When a request comes in, it’s handled asynchronously. The server doesn’t need to wait for the database to respond or the file system to finish reading — it moves on to handle the next request.
Here’s how it works:
This allows Node.js to handle thousands of requests simultaneously, even though it operates on a single thread.
Example:
const http = require('http');
const server = http.createServer((req, res) => {
setTimeout(() => {
res.end('Request handled');
}, 1000); // Simulate asynchronous operation
});
server.listen(3000, () => {
console.log('Server is running on port 3000');
});
In this example, Node.js processes requests asynchronously. Even if one user’s request takes a while, the server can handle other users’ requests without being blocked.
Associate Software Engineer@Kaz Software | Ex-Rokomari SWE | Beta MLSA | ICPC Regionalist
5 个月Very informative Keep sharing bro