Every person learning to code feels happy while coding a synchronous single-threaded program. Eventually, when encountering resource-intensive or complex tasks, we must knock on the door of either asynchronous or multithreaded coding.
How coders–or should I call programmers–view the two different programming paradigms is greatly influenced by the language through which he has learned programming. If it were the 2010s, I would assume that your first language is C, Java, or C++. But we are in 2025, and I have seen most newcomers start learning Python or Javascript. If you have started or do most of the programming in Javascript, you would definitely used async/await, even if you don't know what it does. If you come from different language like Python, C, or Java, you might have heard about threading or multithreading even though you had got no chance of using it in your project. Before jumping into conclusion on what is best for you, first let's see what each one does.
Multithreaded Coding
Before talking about multithreading, first let me tell you what a thread is really.
In simple words, a thread is a sequence of tasks that need to be performed by the CPU. In your system, even if you are just reading this article, there might be thousands of threads running. My system has only 8 cores, but how can it run thousands of threads simultaneously–this question might arise in your mind. Don't be confused with CPU cores and threads. Threads are managed by your operating system; it switches between different threads and run them. The switching is so fast that it gives a sense of multiprocessing. Due to threads, you can run multiple software even if you have single core system. Even if you have 4 4-core CPU system, you can spawn thousands of threads through your program. However, these threads are managed by the operating system, and switching between such large number of threads is not effective. That's why you will see most programs only spawn the number of threads which is equal to the number of CPU cores.
But on the other hand, modern languages like Go have introduced goroutines, which are light threads managed by the Go complier. Additionally, in Java 21, virtual threads were introduced. These light threads managed by the compilers or runtimes are more superior in performance and concurrency than the OS based threads, which we can see in C and C++.
Now, you know what a thread is. You might have guessed what multithreaded coding is, if you haven't, multithreading coding is spawning multiple threads to run multiple codes simultaneously. Running multiple parts of code in parallel might seem fascinating, right? It is not as cool as you think. If those codes running in parallel have to access or modify the same resource or data, you will come across a problem called race condition. But it is not a big deal, you can solve it using a concept called mutex (Mutual Exclusive), which means preventing access to the same data or resource at single time. Once a mutex variable is declared, if two concurrent threads try to access it at the same time, the one that attempts to access it first will succeed, while the other thread will be suspended until the mutex is released.
Asynchronous Code
Unlike multithreaded code, asynchronous code typically runs on a single thread. Asynchronous code operates sequentially within an event loop. When the program encounters the await
keyword in an asynchronous function, the execution of that function is paused at that point, allowing the event loop to handle other tasks. Once the awaited operation completes, the event loop resumes the paused function from where it left off. Let's see it in action with an example of async code in Node.js.
async function deleteUser(id){
const user = await User.findOne(id);
if(user){
await User.delete(id);
return true;
}
else{
console.log("User not found");
return false;
}
}
When the deleteUser
function is invoked, the Node.js runtime executes the first line of the function, which calls User.findOne(id)
. Since findOne
is an asynchronous operation and the await
keyword is used, the runtime pauses execution of the deleteUser
function at this point and moves on to handle other tasks in the event loop.
While waiting for the User.findOne
operation to resolve, the runtime continues serving other requests. The call stack retains a reference to this paused function and its associated variables. On each iteration of the event loop, the runtime checks whether the findOne
operation has resolved. Once it completes, the resolved value is assigned to the user
variable.
If a user is found (if (user)
), the function proceeds to the next await
statement, where User.delete(id)
is invoked. Similarly, the runtime pauses execution of the function at this point and resumes handling other tasks. On a subsequent iteration of the event loop, once the delete
operation resolves, the function returns true
.
If no user is found (else
block), the runtime logs "User not found" to the console and returns false
.
Unlike multithreaded code, there is no risk of race conditions in asynchronous code because, at any given time, only one function or part of the code can access a variable. If your program relies on external services such as API calls, database queries, or system calls, asynchronous code is highly suitable, but not for CPU intensive tasks.
Choose Multithreading Over Async:
- When your task is CPU-intensive.
- When your task does not rely on external APIs, databases, or system calls.
Choose Async Over Multithreading:
- When your task is not CPU-intensive.
- When your task heavily relies on external APIs, databases, or system calls.
Best Approach:
Combine multithreading with asynchronous code when appropriate.
For example, in an image processing backend:
- Use multithreading to handle CPU-intensive tasks, such as processing the image.
- Use asynchronous code to perform I/O-bound tasks, like saving the processed image to the system.
If you use Go with goroutines, you don't need to worry about choosing between multithreading or asynchronous programming, as Go's concurrency model efficiently handles both.