documentation/blog/2024-10-24-async-vs-sync.md
This article was last updated on October 24, 2024, to include modern asynchronous patterns, error handling techniques, and practical examples using promises and async/await.
The decision between synchronous and asynchronous programming models is not just a technical one in software development; it affects how programs work together, complete tasks, and react to user inputs.
It's important to remember that choosing the right model can make or break a project as we compare these paradigms. The goal of this article is to clear up some ambiguity about these concepts by making a clear distinction between synchronous and asynchronous programming and explaining their pros, cons, and best uses. By understanding what each strategy is really about, developers can make smart decisions and make their approach match the needs of their applications.
Steps we'll cover in this article include:
In synchronous programming, tasks are done sequentially. As with a book, you start at the beginning and read every word and line. Synchronous programming requires the completion of each task before starting the next. Control flow is predictable and simple.
The system can become stuck or unresponsive if one task takes too long. Blocking behavior is a hallmark of synchronous programming.
The synchronous programming model advances operations linearly. The process is simplified as below:
Program execution is sequential. Asks are executed in code sequence. From the top down, it executes each line of code.
If a job takes a long time, such as reading a huge file or waiting for human input, the program blocks until it finishes. Synchronous programming blocks.
Synchronous programming is particularly useful in scenarios where tasks need to be executed in a specific order. For example, if you’re baking a cake, you can’t put it in the oven before you’ve mixed the ingredients. Similarly, in a program, you might need to fetch data from a database before you can process it.
Here’s an example of how synchronous programming might work in the context of reading files:
function readFilesSequentially(fileList) {
for each file in fileList {
content = readFile(file) // This is a blocking operation
process(content)
}
}
In this pseudocode, readFile(file) is a synchronous operation. The process(content) function won’t be called until readFile(file) has completely finished reading the file. This is a clear demonstration of the sequential and blocking nature of synchronous programming.
Asynchronous programming is a paradigm that allows tasks to be executed concurrently, rather than sequentially. This means that the execution of the program doesn’t have to wait for a task to complete before moving on to the next one. It’s like being at a buffet - you don’t have to wait for one person to finish serving themselves before you can start.
In asynchronous programming, tasks are often started and then put aside, allowing other tasks to run in the meantime. Once the original task has been completed, it can be picked up where it left off. This non-blocking nature is a key characteristic of asynchronous programming.
Concurrent Execution: One of the main aspects of asynchronous programming is the ability to execute multiple tasks concurrently. This can lead to a significant increase in the efficiency and performance of the program, especially in scenarios where tasks are independent or require waiting for some external resource, such as a network request.
Non-Blocking Nature: Asynchronous programming doesn't block the rest of the program because it doesn't wait for long tasks like I/O operations. In UI programming, this can improve user experience and responsiveness.
I/O-bound tasks are often programmed asynchronously. Asynchronous tasks can be used in web development to make API queries, access databases, and handle user input without interrupting the main thread.
Asynchronous programming can be used to make AJAX queries in web development. See below example:
function fetchAndDisplayData(url) {
// This is a non-blocking operation
data = asyncFetch(url);
data.then((response) => {
// This code will run once the data has been fetched
displayData(response);
});
}
In the above pseudocode, asyncFetch(url) is an asynchronous operation. The displayData(response) function will not be called until asyncFetch(url) has finished fetching the data. Meanwhile, other code can continue to run in the background which demonstrates the non-blocking nature of asynchronous programming.
"www.medium.com/from-the-scratch/wtf-is-synchronous-and-asynchronous-1a75afd039df"
Differences between synchronous and asynchronous programming in terms of performance, application execution, and execution time are as below:
Synchronous: Tasks are executed sequentially, one after the other, in a synchronous system. The result is a control flow that is easy to foresee and implement.
Asynchronous: Tasks can be executed simultaneously in an asynchronous environment. This eliminates the need for the software to wait for one task to finish before proceeding to the next.
Synchronous: With synchronous performance, if a single job takes too long to finish, the whole system could freeze or become unresponsive.
Asynchronous: The non-blocking aspect of asynchronous programming can result in a more responsive and seamless user experience, especially when it comes to user interface development.
Synchronous: Ideal for situations requiring the execution of steps in a predetermined sequence.
Asynchronous: When jobs are I/O bound instead of CPU bound, they are considered asynchronous.
Web based applications
To avoid interrupting the main thread of execution, asynchronous tasks can be utilized to do operations like performing API queries.
Database Management
Data reading and writing operations might be tedious and don't necessarily have to be finished before going on to other tasks.
UI programming
A more responsive and fluid user experience is possible with asynchronous programming when dealing with user input.
File I/O operations
As a general rule, you don't have to finish time-consuming file I/O operations before going on to the next step.
In JS, working with async code effectively involves understanding its event loop and call stack. Very simply, it’s where the call stack goes through executing code in order. It executes synchronous tasks first and finally lets the event loop jump in to handle any asynchronous code statements, stuff like setTimeout or API calls, after it has processed the synchronous code.
This is how JavaScript can appear as if it’s doing lots of things all at once, even though technically it’s single-threaded. While these async operations are running, the event loop makes sure that all data is being processed at the right time without blocking the main thread.
Understanding how the event loop and call stack interact helps us write better asynchronous code and prevents common problems like UI freezes or very slow-feeling user interaction.
The next highly employable tool for managing stuff asynchronously would be Web Workers. They enable us to run JavaScript in the background without blocking the main thread, and that is super helpful when it comes to performance and stuff we have to execute, like complex calculations or fetching a lot of data. Web Workers are giving us true parallelism, meaning we could offload heavy work into another thread and keep the main UI responsible. One thing to keep in mind, though, is that the Workers don’t have access to the DOM and thus are best for things that don’t require refreshing the user interface directly.
Here is a quick example of how we might utilize Web Workers:
// In the main script
const worker = new Worker("./worker.js");
worker.postMessage("Start the task");
// In the worker script (worker.js)
onmessage = function (event) {
// Perform long-running task here
postMessage("Task done");
};
Sequential data retrieval and processing
For certain programs, retrieving data from a database is a prerequisite to processing that data.
Composing basic scripts
When working with small scripts, synchronous programming might be more understandable and debug-friendly.
CPU-bound tasks
Carrying out operations that are CPU-intensive. Synchronous programming may be more efficient if a task is CPU-bound instead than I/O-bound.
Below we have compiled the comparison table summarizing the differences between the two approaches.
| Criteria | Synchronous Programming | Asynchronous Programming |
|---|---|---|
| Execution Pattern | Executes tasks one after another in a linear sequence. | Executes multiple tasks concurrently, without waiting for one to finish before starting another. |
| Blocking Behavior | Blocks the execution flow until the current task is completed. | Allows the execution to continue with other tasks, utilizing a non-blocking approach. |
| Performance Impact | May lead to inefficiencies in scenarios involving waiting, as the system remains idle during task execution. | Enhances efficiency and responsiveness, particularly for I/O-bound operations, by not wasting idle time. |
| Ideal Use Cases | Well-suited for tasks that must be completed in order and where each step depends on the completion of the previous one. | Best for situations requiring high responsiveness or handling operations that involve waiting, such as I/O activities. |
| When to Use | - When tasks need to be processed in strict sequence. |
In synchronous programming, tasks are processed sequentially. Here’s an example in Python:
import time
def process_userData(task):
# Simulate task processing time
time.sleep(1)
print(f"Task {task} processed")
tasks = ['task1', 'task2', 'task3']
for task in tasks:
process_userData(task)
Jobs are processed sequentially by this synchronous method process_userData. If one job takes too long to finish, subsequent tasks will have to wait because of this sequential processing, which can cause delays. The application's performance and the user's experience may suffer as a result.
In contrast, asynchronous programming allows for concurrent task processing. Here’s an example in Python using the asyncio library:
import asyncio
async def retrieve_data(source):
# Simulate time taken to fetch data
await asyncio.sleep(1)
print(f"Data retrieved {source}")
sources = ['source1', 'source2', 'source3']
async def main():
tasks = retrieve_data(source) for source in sources]
await asyncio.gather(*tasks)
asyncio.run(main())
The asynchronous method starts numerous processes at the same time. This ensures that the application can jump from one task to another without interruption. We can enhance the application's performance and user experience by doing this. However, dealing with tasks and callbacks can make it more difficult to implement.
console.log("Start"); // First task (synchronous) - goes to call stack
setTimeout(() => {
console.log("Timeout callback"); // This task(aysnchronous) is put into the event loop
}, 1000);
console.log("End"); // Second task (synchronous) - in call stack
Call Stack:
Event Loop:
Output:
Start
End
Timeout callback
This example shows how JavaScript executes synchronous tasks first, then processes asynchronous tasks using the event loop once the main call stack is clear.
The most common pattern in synchronous programming is the sequential execution pattern, where tasks are executed one after the other.
“Callback hell” refers to heavily nested callbacks that make the code hard to read and understand. Here are some ways to avoid it:
I would like to share some tips on how we can manage memory more effectively when working with asynchronous programming since improper handling could lead to some performance issues, such as memory leaks.
When working with asynchronous code, it is really important to pay attention to how memory is allocated and how it gets cleaned up. It has something to do with long-running tasks or promises that are not being settled, which could lead to memory leaks if not handled correctly.
In JavaScript, memory is looked after by the garbage collector. The garbage collector automatically cleans up memory that is no longer used by an application. However, when using asynchronous programming, it can be easy for the memory to hang around for longer than it needs to if we’re not careful. For instance, promises that never resolve, event listeners that are still attached, or running timers will hold onto larger chunks of memory than necessary.
let pendingPromise = new Promise(function (resolve, reject) {
// This promise never resolves
});
element.addEventListener("click", handleClick);
// Forgetting to remove the listener
// element.removeEventListener('click', handleClick);
Timers: Using setTimeout or setInterval without clearing them when they are no longer needed can also lead to memory being held longer than necessary.
var timer = setInterval(function () {
console.log("Running.");
}, 1000);
// Forgetting to clear the interval
// clearInterval(timer);
let myPromise = new Promise((resolve, reject) =>
setTimeout(() => {
resolve("Task complete");
}, 1000),
);
myPromise.then((result) => console.log(result));
element.addEventListener("click", handleClick);
// Proper cleanup when no longer needed
element.removeEventListener("click", handleClick);
var interval = setInterval(function () {
console.log('Doing something...');
}, 1000);
// Clear the interval when done
clearInterval(interval);
Another advanced technique involves the use of WeakMap or WeakSet to manage objects that may be garbage-collected automatically once they become referenced nowhere else in your code. These structures will let you reference objects without preventing them from being garbage-collected.
let myWeakMap = new WeakMap();
let obj = {};
myWeakMap.set(obj, "someValue");
// If obj gets dereferenced somewhere else, it will be garbage-collected.
obj = null;
As we come to the end of our discussion of synchronous and asynchronous programming models, it's clear that each has its benefits that are best for certain situations. Because it works in a sequential and blocking way, synchronous programming is easy to understand and is great for tasks that need to be done linearly.
On the other hand, asynchronous programming, which is known for not blocking and allowing multiple tasks to run at the same time, works best when high responsiveness and efficiency are needed, especially in I/O-bound operations. Which of these approaches to use depends on how well you understand the application's needs, performance issues, and the user experience you want.
With the information from this comparison, developers are more prepared to handle the complicated world of software engineering and make sure their projects are not only useful but also the best ones for the job.