Multi-threading and Asynchronous Programming in Java Script

Shehan PW
6 min readMar 11, 2023

--

non blocking I/O, event loop, async/await, callbacks, workers

Multithreading and asynchronous programming are both important techniques for writing efficient and responsive software in JavaScript. While they share some similarities, they are fundamentally different approaches to handling concurrency and can be better suited to different types of tasks and use cases.

Multithreading involves running multiple threads of execution simultaneously within a single process, allowing for parallel processing of tasks. In JavaScript, multithreading is typically implemented using Web Workers, which run JavaScript code in separate threads and communicate with the main thread using message passing.

Asynchronous programming, on the other hand, involves executing tasks out of order and without blocking the main thread of execution. In JavaScript, asynchronous programming is commonly achieved using callbacks, promises, and the async/await syntax. Asynchronous programming is well-suited to handling I/O-bound tasks, such as making network requests or reading from a file, where the performance bottleneck is often the time it takes to wait for a response.

One of the main advantages of multithreading is that it allows for true parallel processing of tasks, which can lead to significant performance improvements for CPU-bound tasks, such as image processing or scientific computing. In contrast, asynchronous programming can be more efficient for I/O-bound tasks, where the performance bottleneck is often waiting for a response from an external system, and the main thread can be freed up to handle other tasks in the meantime.

However, multithreading can also introduce new challenges and complexities, such as race conditions, deadlocks, and shared memory issues. Asynchronous programming, while not immune to these issues, can be simpler and more predictable to work with, especially when using higher-level abstractions such as promises and async/await.

In terms of code readability and maintainability, both multithreading and asynchronous programming can have their advantages and disadvantages. Multithreading can lead to more complex code with more moving parts, while asynchronous programming can lead to callback hell and hard-to-read code if not managed properly.

example of call back hell:-

function fetchData(url, callback) {
// make an API request to the given URL
// and pass the result to the callback function
makeApiRequest(url, function(response) {
// process the response
processResponse(response, function(result) {
// do something with the result
doSomething(result, function(finalResult) {
// do something with the final result
callback(finalResult);
});
});
});
}

Overall, the choice between multithreading and asynchronous programming in JavaScript depends on the specific requirements and constraints of the task at hand. For CPU-bound tasks that can benefit from true parallel processing, multithreading may be the best choice. For I/O-bound tasks or tasks that require a more sequential flow of execution, asynchronous programming may be more appropriate. It’s important to understand the strengths and limitations of each approach and choose the best tool for the job.

Multithreading in java-script

JavaScript is a single-threaded language, which means that it can only execute one task at a time. However, JavaScript does support multithreading through the use of Web Workers.

Web Workers are a feature of modern web browsers that allow JavaScript code to run in separate threads, allowing for parallel execution of tasks. Web Workers are isolated from the main JavaScript thread, which means they can’t access the DOM or other global variables, but they can communicate with the main thread using message passing.

Here’s an example of using Web Workers for multithreading in JavaScript:

// Create a new Web Worker
const worker = new Worker('worker.js');

// Send a message to the worker
worker.postMessage({ task: 'someTask' });

// Receive a message from the worker
worker.onmessage = function(event) {
console.log('Received message from worker:', event.data);
};

// Handle errors from the worker
worker.onerror = function(error) {
console.error('Error from worker:', error.message);
};

In this example, we create a new Web Worker using the Worker constructor, passing in the URL of a JavaScript file that contains the code to run in the worker thread. We then send a message to the worker using the postMessage method.

In the worker thread, we can listen for messages using the onmessage event handler, and respond to them using the postMessage method. We can also handle errors using the onerror event handler.

Note that Web Workers have some limitations, such as not being able to access the DOM or global variables, and not being supported in all web browsers. They’re also not a replacement for true multithreading, as they don’t allow for shared memory between threads. However, they can be a useful tool for offloading CPU-intensive tasks to separate threads and improving the performance of web applications.

Non Blocking I/O

In JavaScript, non-blocking I/O (input/output) is an essential technique for building efficient and responsive web applications. Non-blocking I/O allows the program to continue executing while waiting for I/O operations, such as network requests or file I/O, to complete. This means that the program can handle multiple requests simultaneously without blocking the main thread of execution.

JavaScript achieves non-blocking I/O using an event-driven programming model. In an event-driven model, the program listens for and responds to events, rather than executing code sequentially. When an I/O operation is initiated, such as making an HTTP request, the program does not block and wait for the operation to complete. Instead, the program registers a callback function to be executed when the operation completes. Meanwhile, the program can continue executing other tasks, responding to user input, or handling other requests.

Node.js, a popular JavaScript runtime environment, is built on this event-driven model and provides a rich set of asynchronous I/O APIs that enable non-blocking I/O. For example, the core http module in Node.js provides an HTTP server that is non-blocking and scalable. When an HTTP request is received, the server registers a callback function to be executed when the request is complete. Meanwhile, the server can continue handling other requests.

Non-blocking I/O is critical for building performance and scalable web applications. By using event-driven programming and asynchronous I/O APIs, developers can build applications that are responsive and can handle a large number of concurrent requests without blocking the main thread of execution. In addition, non-blocking I/O can greatly improve the user experience of web applications, reducing the time it takes for the user to see content and interact with the application.

Asynchronous programming in javascript

async function fetchData(url) {
try {
const response = await makeApiRequest(url);
const result = await processResponse(response);
const finalResult = await doSomething(result);
return finalResult;
} catch (error) {
console.error(error);
}
}

In this example, we have an async function called fetchData that makes an API request to a given URL and then processes the response using three separate functions. We use the await keyword to pause the execution of the function until each asynchronous operation completes. If any of the asynchronous operations fails, we catch the error using a try...catch block.

The fetchData function returns a Promise that resolves with the final result if all of the asynchronous operations complete successfully. If any of the asynchronous operations fail, the Promise is rejected with an error.

Using async/await makes the code more concise and easier to read than using nested callbacks or Promises. It also provides a more synchronous-like way of writing asynchronous code, which can be helpful for developers who are new to asynchronous programming.

Writing a async function from scratch

function makeApiRequest(url) {
return new Promise((resolve, reject) => {
// make an API request to the given URL
// and resolve the Promise with the response
// or reject the Promise with an error
fetch(url)
.then(response => {
if (response.ok) {
resolve(response.json());
} else {
reject(new Error('API request failed'));
}
})
.catch(error => reject(error));
});
}

async function fetchData() {
try {
const data = await makeApiRequest('https://example.com/api/data');
console.log(data);
} catch (error) {
console.error(error);
}
}

In this example, we define a function called makeApiRequest that makes an API request to a given URL and returns a Promise that resolves with the response data or rejects with an error if the request fails.

We then define an async function called fetchData that uses the await keyword to pause the execution of the function until the makeApiRequest Promise resolves or rejects. If the Promise resolves successfully, the response data is logged to the console. If the Promise is rejected with an error, the error is logged to the console.

Note that in order to use the await keyword, the function must be marked with the async keyword, which indicates to the JavaScript engine that the function contains asynchronous operations.

--

--

Shehan PW

Full-stack web developer | Block-chain developer . (MERN stack && MARN stack). System Design and Development || NodeJS || JavaScript || Java || REACT || etc