15 minlesson

async and await Keywords

async and await Keywords

The async and await keywords make asynchronous code look and behave like synchronous code, dramatically simplifying async programming.

The Problem with Callbacks

Before async/await, async code used callbacks:

csharp
1// Old callback style - "callback hell"
2DownloadData(url, data =>
3{
4 ProcessData(data, processed =>
5 {
6 SaveData(processed, () =>
7 {
8 Console.WriteLine("Done!");
9 });
10 });
11});

With async/await:

csharp
1// Modern async/await - reads like sync code
2var data = await DownloadDataAsync(url);
3var processed = await ProcessDataAsync(data);
4await SaveDataAsync(processed);
5Console.WriteLine("Done!");

How async/await Works

The async Modifier

Marks a method as asynchronous:

csharp
1async Task DoWorkAsync()
2{
3 // Method can now use await
4 await Task.Delay(1000);
5}

Key points:

  • async alone doesn't make code run asynchronously
  • It enables the use of await in the method
  • The compiler transforms the method into a state machine

The await Keyword

Asynchronously waits for a task to complete:

csharp
1async Task<string> GetDataAsync()
2{
3 // Thread is RELEASED here, not blocked
4 string result = await httpClient.GetStringAsync("https://api.example.com");
5
6 // Execution resumes here when data arrives
7 return result;
8}

What await does:

  1. Checks if the task is already complete
  2. If not, registers a continuation and returns (releases the thread)
  3. When task completes, resumes execution at the await point

Return Types

Task (no return value)

csharp
1async Task PrintMessageAsync()
2{
3 await Task.Delay(1000);
4 Console.WriteLine("Hello!");
5}

Task (returns a value)

csharp
1async Task<int> CalculateAsync()
2{
3 await Task.Delay(1000);
4 return 42;
5}
6
7// Calling code
8int result = await CalculateAsync();

async void (event handlers only!)

csharp
1// ONLY use for event handlers
2async void Button_Click(object sender, EventArgs e)
3{
4 await DoWorkAsync();
5}
6
7// NEVER use async void for regular methods!
8// Exceptions can't be caught, can't be awaited

State Machine Transformation

The compiler transforms async methods into state machines:

csharp
1// What you write:
2async Task<int> GetValueAsync()
3{
4 Console.WriteLine("Before");
5 await Task.Delay(1000);
6 Console.WriteLine("After");
7 return 42;
8}
9
10// Compiler generates something like:
11Task<int> GetValueAsync()
12{
13 var stateMachine = new GetValueAsyncStateMachine();
14 stateMachine.state = 0;
15 stateMachine.MoveNext();
16 return stateMachine.Task;
17}

The state machine tracks:

  • Current position in the method
  • Local variables
  • Where to resume after each await

Execution Flow

csharp
1async Task ExampleAsync()
2{
3 Console.WriteLine("1. Start"); // Runs on original thread
4
5 await Task.Delay(1000); // Thread released here
6
7 Console.WriteLine("2. After delay"); // May run on different thread
8
9 await Task.Delay(500); // Thread released again
10
11 Console.WriteLine("3. Done"); // May run on yet another thread
12}
1Thread Timeline:
2┌─────────┐ ┌─────────┐ ┌─────────┐
3│ "Start" │ │ "After" │ │ "Done" │
4│Thread 1 │ │Thread 2 │ │Thread 3 │
5└────┬────┘ └────┬────┘ └────┬────┘
6 │ │ │
7 └──────────────────►└──────────────────►│
8 await (released) await (released)

Multiple Awaits

Each await is a potential suspension point:

csharp
1async Task ProcessAsync()
2{
3 var data = await FetchDataAsync(); // Pause 1
4 var processed = await ProcessAsync(data); // Pause 2
5 await SaveAsync(processed); // Pause 3
6
7 // Each await may resume on a different thread
8}

Awaiting Multiple Tasks

csharp
1// Sequential (slower)
2var result1 = await GetData1Async();
3var result2 = await GetData2Async();
4// Total time: time1 + time2
5
6// Parallel (faster)
7var task1 = GetData1Async();
8var task2 = GetData2Async();
9var result1 = await task1;
10var result2 = await task2;
11// Total time: max(time1, time2)
12
13// Or use WhenAll
14var results = await Task.WhenAll(task1, task2);

Common Mistakes

Forgetting to await

csharp
1// BUG: Task not awaited, runs in background
2DoWorkAsync(); // No await!
3
4// CORRECT:
5await DoWorkAsync();

Using async void

csharp
1// BAD: Can't catch exceptions, can't await
2async void DoWork() { ... }
3
4// GOOD: Return Task
5async Task DoWorkAsync() { ... }

Blocking on async code

csharp
1// BAD: Can cause deadlocks
2var result = GetDataAsync().Result;
3
4// GOOD: Use await
5var result = await GetDataAsync();

Key Takeaways

  • async enables await in a method
  • await releases the thread while waiting
  • Return Task or Task<T>, avoid async void
  • The compiler creates a state machine
  • Execution may resume on different threads
  • Don't block with .Result or .Wait() - use await