NeuroAgent

How and When to Use Async/Await in C#

Learn how async/await works in C# without creating background threads. Understand timing, thread execution, and best practices for responsive applications.

Question

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?

csharp
// 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:

  1. Why methods using await must be marked as async
  2. How thread execution works with async and await
  3. The timing of code execution in async methods
  4. Whether async/await automatically creates background threads
NeuroAgent

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

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:

  1. Compiler Requirement: The C# compiler needs to detect methods containing await keywords to transform them into state machines. Without the async modifier, the compiler would generate a compilation error.

  2. 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.

  3. Return Type Flexibility: The async modifier allows methods to return Task, Task<T>, or void (for event handlers), which are essential for the asynchronous programming model.

Important: Only methods that contain await or return Task/return Task<T> need to be marked as async. Marking a method as async without using await creates unnecessary overhead.

How Thread Execution Works with Async/Await

The execution flow in your example works as follows:

csharp
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 Task or Task<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:

  1. Initial Execution: button1_Click starts executing on the UI thread
  2. Task Creation: DoSomethingAsync() is called and immediately returns a Task<int> without waiting
  3. Immediate Execution: int a = 1; executes right away because it’s not dependent on the task
  4. Suspension Point: When await access; is reached, the method suspends
  5. Return to Caller: Control returns to the UI message pump, keeping the UI responsive
  6. Continuation: After DoSomethingAsync completes, the rest of button1_Click resumes 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:

csharp
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:

csharp
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

csharp
// 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

csharp
// 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

csharp
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

  1. C# Async/Await Explained: Complete Guide with Examples [2025] - NDepend Blog
  2. Asynchronous programming scenarios - C# | Microsoft Learn
  3. How Async/Await Really Works in C# - .NET Blog
  4. C# Asynchronous Programming: Tasks, Threads, and Async/Await | Medium
  5. Asynchronous programming - C# | Microsoft Learn
  6. The Task Asynchronous Programming (TAP) model with async and await - Microsoft Learn
  7. Asynchronous programming with async, await, Task in C# - TutorialTeacher
  8. If async-await doesn’t create any additional threads, then how does it make applications responsive? - Stack Overflow
  9. Difference Between Asynchronous and Multithreading in C# - Code Maze
  10. 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.Run to 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.