How and when to use ‘async’ and ‘await’ in C#
From my understanding, one of the main purposes of async and await is to make code easier to write and read. But does using them actually spawn background threads to perform long-running operations?
I’m trying to understand the basic behavior of async/await with the following example. Can someone clarify these points for me?
// Why must this method be marked as `async`?
private async void button1_Click(object sender, EventArgs e)
{
Task<int> access = DoSomethingAsync();
// Task-independent code here
// This line is reached after the 5-second sleep from
// DoSomethingAsync(). Shouldn't it be reached immediately?
int a = 1;
// From my understanding, the waiting should happen here.
int x = await access;
}
async Task<int> DoSomethingAsync()
{
// Is this code executed on a background thread?
System.Threading.Thread.Sleep(5000);
return 1;
}
Specifically, I’d like to understand:
- Why methods using
awaitmust be marked asasync - How thread execution works with
asyncandawait - The timing of code execution in async methods
- Whether
async/awaitautomatically creates background threads
The async and await keywords in C# provide a powerful way to write asynchronous code that appears more linear and readable, but they don’t automatically create background threads. Instead, they use a state machine mechanism to manage execution flow without blocking threads.
Contents
- Understanding Async/Await Basics
- Why Methods Using Await Must Be Marked Async
- How Thread Execution Works with Async/Await
- Timing of Code Execution in Async Methods
- Do Async/Await Automatically Create Background Threads?
- Best Practices and Common Patterns
- Troubleshooting Common Issues
Understanding Async/Await Basics
The await keyword provides a nonblocking way to start a task, then continue execution when the task completes. Unlike traditional asynchronous programming that required complex callbacks, async/await lets you write asynchronous code that reads like synchronous code.
When you call an asynchronous method, it starts running on the current thread until it hits an await keyword. At that point, the method execution is paused, and control is returned to the calling method source.
Why Methods Using Await Must Be Marked Async
Methods that use await must be marked with the async keyword because:
-
Compiler Requirement: The C# compiler needs to detect methods containing
awaitkeywords to transform them into state machines. Without theasyncmodifier, the compiler would generate a compilation error. -
State Machine Generation: When you mark a method as
async, the compiler transforms it into a state machine that can pause and resume execution. This state machine tracks the current execution point and manages the continuation when the awaited operation completes. -
Return Type Flexibility: The
asyncmodifier allows methods to returnTask,Task<T>, orvoid(for event handlers), which are essential for the asynchronous programming model.
Important: Only methods that contain
awaitorreturn Task/return Task<T>need to be marked asasync. Marking a method asasyncwithout usingawaitcreates unnecessary overhead.
How Thread Execution Works with Async/Await
The execution flow in your example works as follows:
private async void button1_Click(object sender, EventArgs e)
{
// 1. Method starts on the UI thread
Task<int> access = DoSomethingAsync();
// Task-independent code here - executes immediately
int a = 1;
// 2. When this line is reached, the method is suspended
// and control returns to the calling context
int x = await access;
// 3. This code executes only after DoSomethingAsync completes
// (after the 5-second sleep)
}
According to Microsoft Learn, inside an async method:
- I/O-bound code starts an operation represented by a
TaskorTask<T>object - CPU-bound code should be started on a background thread with
Task.Run
Timing of Code Execution in Async Methods
In your example, the timing works like this:
- Initial Execution:
button1_Clickstarts executing on the UI thread - Task Creation:
DoSomethingAsync()is called and immediately returns aTask<int>without waiting - Immediate Execution:
int a = 1;executes right away because it’s not dependent on the task - Suspension Point: When
await access;is reached, the method suspends - Return to Caller: Control returns to the UI message pump, keeping the UI responsive
- Continuation: After
DoSomethingAsynccompletes, the rest ofbutton1_Clickresumes execution
The confusion comes from the fact that Task<int> access = DoSomethingAsync(); doesn’t actually wait for the operation to complete - it just creates the task. The waiting happens at the await point.
Do Async/Await Automatically Create Background Threads?
No, async and await don’t automatically create background threads. This is a crucial misconception that many developers have.
As Microsoft Learn clearly states:
The async and await keywords don’t cause extra threads to be created. Async methods don’t require multithreading because an async method doesn’t run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is actually doing work.
In your example:
async Task<int> DoSomethingAsync()
{
// This code runs on the same thread that called it
System.Threading.Thread.Sleep(5000);
return 1;
}
The Thread.Sleep(5000) actually blocks the thread it’s running on, which defeats the purpose of async programming. For CPU-bound work like this, you should use Task.Run:
async Task<int> DoSomethingAsync()
{
// This runs on a background thread from the thread pool
return await Task.Run(() =>
{
System.Threading.Thread.Sleep(5000);
return 1;
});
}
Best Practices and Common Patterns
1. Use Async All the Way
// Don't do this - blocking async code
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDatabaseAsync();
Thread.Sleep(1000); // Blocks a thread
return result;
}
// Do this - truly async
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDatabaseAsync();
await Task.Delay(1000); // Non-blocking delay
return result;
}
2. ConfigureAwait for Library Code
// For library code, use ConfigureAwait(false)
public async Task<int> CalculateAsync()
{
// Don't need to return to original context for work
var result = await DoWorkAsync().ConfigureAwait(false);
return result;
}
3. Handle Exceptions Properly
try
{
var result = await SomeOperationAsync();
}
catch (Exception ex)
{
// Handle exceptions from async operations
}
Troubleshooting Common Issues
Deadlocks
Common when mixing blocking and async code, especially on UI threads.
Thread Pool Exhaustion
Can occur when running too many CPU-bound operations with Task.Run without proper limits.
Async Void Methods
Should only be used for event handlers. Regular async methods should return Task or Task<T>.
In summary, async and await are compiler-level constructs that manage execution flow without requiring new threads. They make your code more readable and responsive by allowing you to write asynchronous code that appears linear, but the underlying thread management requires understanding the Task-based Asynchronous Pattern (TAP).
Sources
- C# Async/Await Explained: Complete Guide with Examples [2025] - NDepend Blog
- Asynchronous programming scenarios - C# | Microsoft Learn
- How Async/Await Really Works in C# - .NET Blog
- C# Asynchronous Programming: Tasks, Threads, and Async/Await | Medium
- Asynchronous programming - C# | Microsoft Learn
- The Task Asynchronous Programming (TAP) model with async and await - Microsoft Learn
- Asynchronous programming with async, await, Task in C# - TutorialTeacher
- If async-await doesn’t create any additional threads, then how does it make applications responsive? - Stack Overflow
- Difference Between Asynchronous and Multithreading in C# - Code Maze
- C# “async and await” feature and threads - Stack Overflow
Conclusion
- Async/await doesn’t create background threads - it manages execution flow using state machines
- Methods using await must be async because the compiler needs to generate the state machine
- Code timing - the method pauses at await and continues after completion, keeping threads free
- CPU-bound work should use
Task.Runto actually move work to background threads - I/O-bound work benefits from async/await without creating new threads
- Best practice is to use async all the way through your application, avoiding blocking calls in async methods
Understanding these fundamentals will help you write more efficient and responsive applications while avoiding common pitfalls like deadlocks and thread exhaustion.