Updated on
Asynchronous programming is about not holding a thread while waiting. Multithreading is about using more threads to do more work at the same time. They solve different problems and are routinely confused because both are reached for when an application stops responding.
The practical rule follows from that. Waiting on a network call, a database, or a disk read is I/O: use async and await, and the thread goes back to the pool while the wait happens. Work that keeps a CPU busy is compute; that is what extra threads are for.
Asynchronous Programming
Asynchronous programming is a technique where a set of statements runs independently of the main program flow.
We use asynchronous programming when we have a blocking operation in the program and we want to continue with the execution of the program without waiting for the result. This allows us to implement tasks that can run at the same time.
In C#, asynchronous programming is achieved through the use of the async and await keywords. You can learn more about this in our Asynchronous Programming with Async and Await in ASP.NET Core article.
Multithreading
In computer science, a thread is a single continuous flow of control within a program. Multithreading is a technique where the processor uses multiple threads to execute multiple processes concurrently.
We mainly use multithreading when we want to maximize the multi-core processors to have multiple workers working independently.
In C#, the System.Threading namespace contains multiple classes and interfaces for achieving multithreading in the application. To learn more about how to run code in a new thread, you can read our How to Run Code in a New Thread in C# article.
Asynchronous Programming vs Multithreading
They are not alternatives. One is about waiting efficiently, the other about working in parallel, and a program often needs both.
Asynchronous code suspends at an await and releases its thread until the operation it is waiting for completes. Nothing is running during that time: no thread is held, no CPU is used, and the continuation resumes on whatever thread is available afterwards. This is why a single thread can serve thousands of concurrent requests that are mostly waiting.
Multithreading runs several pieces of code at once on several threads. A thread that calls Thread.Sleep or blocks on a lock stays occupied and unavailable for anything else, which is exactly the cost async avoids.
The overlap is that a Task can represent either. Task.Run puts work on a thread pool thread; that is multithreading. await httpClient.GetAsync() holds no thread at all; that is asynchrony. Microsoft’s C# documentation adds the corollary: “Async methods don’t require multithreading because an async method doesn’t run on its own thread.”
Both examples below print the ids of the threads they run on, which is the quickest way to see that difference at runtime. If the question is about the two types rather than the two models, we cover tasks compared with threads separately.
Async Code Example
Let’s see multiple asynchronous operations in action:
public static async Task FirstAsync()
{
Console.WriteLine("First Async Method on Thread with Id: " + Environment.CurrentManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("First Async Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
public static async Task SecondAsync()
{
Console.WriteLine("Second Async Method on Thread with Id: " + Environment.CurrentManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("Second Async Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
public static async Task ThirdAsync()
{
Console.WriteLine("Third Async Method on Thread with Id: " + Environment.CurrentManagedThreadId);
await Task.Delay(1000);
Console.WriteLine("Third Async Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
In each of these asynchronous methods, we write the thread id this method uses when starting the execution. Then, we simulate some work by adding a one-second delay, and finally, we print another message to the console.
Environment.CurrentManagedThreadId is the recommended way to read the current thread id on .NET 10, where analyzer CA1840 is enabled by default and flags the older pattern as a suggestion (Microsoft Learn, read 2026-09-13). It returns the same number as Thread.CurrentThread.ManagedThreadId without materialising the Thread object first.
Now, let’s add another method that will execute these methods:
public static async Task ExecuteAsyncFunctions()
{
var firstAsync = FirstAsync();
var secondAsync = SecondAsync();
var thirdAsync = ThirdAsync();
await Task.WhenAll(firstAsync, secondAsync, thirdAsync);
}
Finally, we are going to modify the Main method:
static async Task Main(string[] args)
{
await ExecuteAsyncFunctions();
}
Let’s run our app and inspect the approximate output:
First Async Method on Thread with Id: 2 Second Async Method on Thread with Id: 2 Third Async Method on Thread with Id: 2 Third Async Method Continuation on Thread with Id: 5 Second Async Method Continuation on Thread with Id: 7 First Async Method Continuation on Thread with Id: 8
We can see that all the operations are starting on the same thread with a number 2. But they are continuing their execution on different threads (5, 7, 8). The exact ids and the order of the continuation lines change from run to run and from machine to machine; the pattern of one starting thread and several continuation threads is what stays the same.
So, why is this happening?
It’s happening because once the thread hits the awaiting operation in FirstAsync, the thread is freed from that method and returned to the thread pool. Once the operation is completed and the method has to continue, a thread is assigned to it from the thread pool. The same process is repeated for the SecondAsync and ThirdAsync as well.
Multithreading Code Example
Now let’s try to implement the same in a multithreaded environment:
public class Multithreading
{
public void FirstMethod()
{
Console.WriteLine("First Multithreading Method on Thread with Id: " + Environment.CurrentManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("First Multithreading Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
public void SecondMethod()
{
Console.WriteLine("Second Multithreading Method on Thread with Id: " + Environment.CurrentManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("Second Multithreading Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
public void ThirdMethod()
{
Console.WriteLine("Third Multithreading Method on Thread with Id: " + Environment.CurrentManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("Third Multithreading Method Continuation on Thread with Id: " + Environment.CurrentManagedThreadId);
}
}
The one-line difference between the two examples carries the whole idea. The async methods await Task.Delay(1000) and hold no thread while they wait; these methods call Thread.Sleep(1000), which parks the thread they are on and keeps it out of circulation for the full second. Our article on Thread.Sleep compared with Task.Delay goes through that distinction on its own.
Also, we need to execute these methods:
public void ExecuteMultithreading()
{
Thread t1 = new Thread(FirstMethod);
Thread t2 = new Thread(SecondMethod);
Thread t3 = new Thread(ThirdMethod);
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
}
Each Join() call blocks the calling thread until that worker finishes. Without them, the method would return the moment the three threads were started, and whether we saw their output at all would depend on process shutdown timing. The calls also demonstrate the cost the article is describing: the caller now holds its own thread doing nothing but waiting.
Finally, we have to modify the Main method:
static async Task Main(string[] args)
{
await ExecuteAsyncFunctions();
Console.WriteLine();
Multithreading multithreading = new Multithreading();
multithreading.ExecuteMultithreading();
}
Let’s see the approximate output to clearly understand how they are different from each other:
First Async Method on Thread with Id: 2 Second Async Method on Thread with Id: 2 Third Async Method on Thread with Id: 2 Third Async Method Continuation on Thread with Id: 5 Second Async Method Continuation on Thread with Id: 7 First Async Method Continuation on Thread with Id: 8 First Multithreading Method on Thread with Id: 10 Second Multithreading Method on Thread with Id: 11 Third Multithreading Method on Thread with Id: 12 Second Multithreading Method Continuation on Thread with Id: 11 Third Multithreading Method Continuation on Thread with Id: 12 First Multithreading Method Continuation on Thread with Id: 10
The first six lines are the async example from the previous section running again, because Main runs both demonstrations in sequence. The multithreading output is everything below the blank line.
We can clearly see the execution of multithreaded methods on different threads as expected. But also, they are keeping the same threads for the continuation compared to the async methods.
From this example, we can see the main difference – Multithreading is a programming technique for executing operations running on multiple threads (also called workers) where we use different threads and block them until the job is done. Asynchronous programming is the concurrent execution of multiple tasks (here the assigned thread is returned back to a thread pool once the await keyword is reached in the method).
Does async/await Create a New Thread?
No. Marking a method async and awaiting inside it creates no thread. Microsoft’s C# documentation is unambiguous: “The async and await keywords don’t cause extra threads to be created.”
The output in the previous section shows what actually happens. All three async methods start on thread 2, and after their awaits complete, the continuations run on threads 5, 7, and 8. Those are thread pool threads that already existed and were idle. The await released thread 2, and the pool assigned whichever thread was free when the work finished.
The multithreading example is the contrast. Three threads are created explicitly and each keeps its own thread all the way through, including while it sleeps.
There is one case where an async method does occupy a thread: Task.Run inside it. That call is a deliberate request to run work on the pool, and it is the right one for CPU-bound work and the wrong one for I/O.
The continuation thread depends on the host. A console application and an ASP.NET Core request have no synchronization context, so the continuation runs on a pool thread as we saw above; a UI framework captures one, and the continuation returns to the UI thread instead. When we do reach for the pool deliberately, the two entry points differ more than they look: our comparison of Task.Run and Task.Factory.StartNew covers which one to prefer.
When Should We Use Async and When Multithreading?
Ask what the code is waiting for.
If it is waiting on something outside the process (an HTTP call, a database query, a file, a message queue), use async and await all the way down. The thread is released for the duration, which is why a web application handles far more concurrent requests this way than by adding threads.
If it is CPU work (resizing images, parsing a large file, running a simulation), threads are what help, and Task.Run or Parallel.ForEach is how to reach them. Making a CPU-bound method async changes nothing about how long it takes.
Mixed work uses both, and the split is usually clean: fetch asynchronously, process in parallel, write asynchronously.
The one combination to avoid is blocking on asynchronous work: .Result or .Wait() on a task, which holds a thread for the whole wait and can deadlock outright.
| Asynchronous programming | Multithreading | |
|---|---|---|
| The problem it solves | A thread sitting idle while waiting | Not enough work happening at once |
| What it costs | Almost nothing while waiting (no thread is held) | One thread per worker, each with its own stack |
| Typical work | Network calls, database queries, file I/O | Image processing, parsing, computation |
| Written with | async, await, Task | Thread, Task.Run, Parallel, the thread pool |
| Threads used | Possibly one, possibly none while awaiting | One per concurrent worker |
| Scales by | Doing more with the threads we have | Adding threads, bounded by cores |
| Where it fails | CPU-bound work (await on a busy loop helps nothing) | I/O waits (a blocked thread is a wasted thread) |
| Continuation thread | Whichever pool thread is free, unless a SynchronizationContext is captured, in which case the continuation returns to it | The same thread throughout |
| In ASP.NET Core | The default for every request path | Reserved for genuine CPU work |
That last point is worth a closer look, because it is the mistake that turns a correct async method into a hung application. Our article on the difference between await and Task.Wait walks through what each one does to the calling thread.
Conclusion
In this article, we have discussed the key differences between asynchronous programming and multithreading. Both techniques are very useful in improving the scalability and performance of the application.
Asynchrony keeps threads free while we wait, multithreading adds threads so more work happens at once, and most real applications use both. For the broader picture of how the async model developed and the other shapes it takes, our guide to asynchronous programming patterns in .NET is the next step.
Tested with .NET 10.0.10.

I still don’t get it. In the first example you start with one thread, and end up with three threads. In the second example you start with three threads and end with three threads. How is the first not multi-threaded if you end up with multiple threads in the end? In your conclusion you’re saying “the assigned thread is returned back to a thread pool” implying that there are multiple threads involved?
Hello Mats. Of course, both examples use multiple threads. As you wrote the last sentence, in the first example, with async the initial thread is always returned to the thread pool (when your code is waiting for some other execution), and then after a new thread is assigned to that request to finish the process. That’s why you see thread 1 for all three requests and different thread numbers once these requests are done. So, both examples are multithreaded, just in the first case, the thread is returned to the thread pool and that same thread can be assigned to another request while the first request is waiting for other executions.
Thank you, I was discussing this with a colleague who insisted that async did not use multiple threads, which is one way to interpret articles like this discussing the differences. My takeaway is rather that with async you don’t control the spawned threads you only get their results whereas with multi-threading you keep control the whole time. There are always multiple threads involved, the difference is how many are “yours”. Is that a fair conclusion?
Well, the most important fact here is that with async you can handle multiple requests at the same time, just if any of those requests reach a blocking operation, they don’t block the used thread, instead, the thread is returned for some other request to use it. But once the blocking operation is done a new thread is assigned to the waiting request so it could be finished (that’s why you see thread 1 as an entry and thread 11,18… as output – this is why I said that async is using multiple threads in my previous reply). That’s the main point of async operations.
Thank you, I think I get it. Async spawns processes more of “on demand” rather than preemptively; i.e. if no blocking operation is encountered then no process is spawned? So a loop with say 100 function calls where only 3 of them get blocked would in essence only use 4 threads instead of 101 if it had been coded as multi-threaded?
Thank you for your thorough explanation. This really illuminated the differences between the two for me, which is something I’ve struggled with. Cheers!
This only works with console applications. Maybe u should explain SynchronizationContexts. Would be kinda nice ngl.
IMO, the type of application is irrelevant here. The concept is what matters. Also, we talked a bit about synchronization context here: https://code-maze.com/asynchronous-programming-with-async-and-await-in-asp-net-core/
But as you will see, we don’t have it in the ASP.NET Core apps.
hi there, good article! I am curious about when the Thread in the Multithreading can be released (freed)? In some production cases, we can get an exception like not enough Thread in the Thread pool to complete the operation. So I would like to know the best way to manage it.
Hello Bac Vo. Thanks. Regarding your question, maybe you can find some answers here
Why did i get this article as recommendation by Android chrome? I am not a C# dev and in nodejs we have async operations and we are only using one thread. But still I used multi threading in C++. So what is the big deal?
Hey Aras, you know, you don’t have to visit the links Google recommends, right? 🙂 As for why it’s recommended to you, not sure we’re the right ones to answer that.
Mariko ..great attempt to explain async and multi thread.
Can you pls clarify below.
a) when the code hits await. , the thread is released and the method called with await executes on its own and not in the original thread where the method with await is called? Where does the method with await gets executed then? Does system handle the execution without a thread internally?
b)the thread releases the execution of method with await and moves to handle the next line of code after the await operation ?
c)can you please explain the flow of execution on how we got this ‘Seconds taken for multithreading operation: 0’? Why would it print after first thread and not after say second or third ? I think I understand but would help if you clarify
Great job again
Hello. Thank you very much for the kind words. To answer a and b questions, it is best for you to read our linked article about async and await. I will paste the link here again https://code-maze.com/asynchronous-programming-with-async-and-await-in-asp-net-core/
For the last question, no it does not print after the first thread. All the operations run in parallel for multithreading so it means they will independently write their results to the console as soon as they are finished. You can call a Sleep method before that Console write operation and you will see the order of messages will change
Nice article, there is room to explain more clearly. While reading the explanation I was feeling both does same thing until I saw the code output, may be start with code output and then your explanation would have been effective and then I wouldn’t needed to read it again.
Thank you for the comment. We’ve updated the explanations so I believe it should be more understandable now. Of course, having the code snippets and the resulting output is always helpful since you can see the difference in practice.
Quote..”Multithreading is an asynchronous operation…”
Not sure that is true ..would the operation running on the thread not need to be “marked” async to be an asynchronous operation?.. otherwise it’s just a normal thread blocking operation.
if I create 4 threads to write four separate large files, those threads are consumed until the job is done.. if my pool of threads is only 4.. then nothing else can run while these 4 threads wait. No matter how many more tasks I have.
Nowif I mark those operations asynchronous then the threads will all start and run, and be release back to the thread pool once the io starts…until it finished.. and at that point each operation will be assigned a thread and continue..in the meantime, other work can be done.. because the thread pool is not consumed..
That is the difference.. this article isn’t quite clear about.
>There is overhead to creating threads as well, may be mention.. not every single small task should be it’s own thread..
My thoughts…
Hello Dave. First of all, thank you for the comment. The first part was fixed, it was meant to sound like a form of async programming, but we modify it anyway. Regarding the second point, I completely support your explanation, but I still think we explained the same. We’ve stated that if the operation is not asynchronous it will be blocked until the job is done, but if it is the thread will be released as soon as the operation begins. Again I agree completely with your explanation and we will see to update our explanation to be even more understandable.
One more time, thanks for the comment. Comments like yours can only make the article better.
So… they’re the same. Got it.
Hello there. Any explanation why do you think that way? I believe we’ve clearly stated what is the difference between the two with examples and explanations – but I would always like to hear your thoughts.