You are viewing a preview of this course. Sign in to start learning

Lesson 1: Async/Await and Task Parallel Library (TPL) Fundamentals

Master asynchronous programming patterns in C# for .NET developer interviews, covering async/await, Task-based operations, and common pitfalls

Lesson 1: Async/Await and Task Parallel Library (TPL) Fundamentals πŸš€

Introduction

Welcome to one of the most frequently tested topics in .NET developer interviews! Asynchronous programming is not just a buzzwordβ€”it's a critical skill for building scalable, responsive applications. In this lesson, we'll dive deep into async/await patterns, the Task Parallel Library (TPL), and the practical scenarios you'll encounter in interviews and production code.

πŸ’‘ Why This Matters: In interviews, you'll often be asked to identify performance bottlenecks, explain threading concepts, or debug async code. Companies want developers who can write non-blocking, efficient code that scales under load.

πŸ€” Did you know? The async/await pattern in C# was introduced in .NET Framework 4.5 (2012) and has since become the standard way to handle asynchronous operations. It's built on top of the Task Parallel Library, which itself was introduced in .NET 4.0.


Core Concepts

1. Understanding Synchronous vs. Asynchronous Execution ⚑

Synchronous code blocks the calling thread until the operation completes. Think of it like standing in line at a coffee shopβ€”you wait until your order is ready before doing anything else.

Asynchronous code allows the calling thread to continue doing other work while waiting for an operation to complete. It's like ordering coffee, getting a buzzer, and sitting down to read while your order is prepared.

SYNCHRONOUS FLOW:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Thread    │──→ [Database Call] ──→ [WAITING...] ──→ [Result] ──→ Continue
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        (Blocked)

ASYNCHRONOUS FLOW:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Thread    │──→ [Start DB Call] ──→ [Do Other Work] ──→ [Result Ready] ──→ Continue
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     (Non-blocking)          ↓
                                    [Thread Pool Handles It]

2. The Task Type πŸ“¦

A Task represents an asynchronous operation. Think of it as a "promise" that work will be completed in the future.

  • Task: Represents an operation that doesn't return a value
  • Task<T>: Represents an operation that returns a value of type T
Task Lifecycle:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Created β”‚ ──→ Initial state
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ WaitingToRun β”‚ ──→ Scheduled but not yet running
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Running β”‚ ──→ Actively executing
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚
     β”œβ”€β”€β†’ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚    β”‚ RanToCompletion β”‚ (Success)
     β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β”œβ”€β”€β†’ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚    β”‚ Faulted β”‚ (Exception)
     β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     └──→ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚ Canceled β”‚ (Cancellation)
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. The async and await Keywords πŸ”‘

async: A method modifier that enables the use of await within the method. It tells the compiler to generate a state machine for the method.

await: Suspends the execution of the async method until the awaited Task completes, then resumes execution with the result.

πŸ’‘ Key Insight: await doesn't block the thread! It yields control back to the caller, allowing the thread to do other work.

Method Signature Rules:

  • Async methods should return Task, Task<T>, or void (avoid void except for event handlers)
  • Convention: Name async methods with "Async" suffix (e.g., GetDataAsync)
+─────────────────────────────────────────────────────────+
β”‚ ASYNC METHOD RETURN TYPES                               β”‚
+──────────────────┬──────────────────────────────────────+
β”‚ Return Type      β”‚ Use Case                             β”‚
+──────────────────┼──────────────────────────────────────+
β”‚ Task             β”‚ Async method that returns no value   β”‚
β”‚ Task<T>          β”‚ Async method that returns value of T β”‚
β”‚ void             β”‚ Event handlers ONLY (fire-and-forget)β”‚
β”‚ ValueTask<T>     β”‚ Performance-critical, may be sync    β”‚
+──────────────────┴──────────────────────────────────────+

4. ConfigureAwait(false) 🎯

When you await a Task, by default, the continuation (code after await) runs on the captured context (usually the UI thread in desktop apps or the ASP.NET synchronization context).

ConfigureAwait(false) tells the runtime: "I don't need to resume on the original context."

🌍 Real-world analogy: Imagine you're a manager who delegates a task. With ConfigureAwait(true), you say "Report back to me specifically." With ConfigureAwait(false), you say "Report to any available manager."

When to use ConfigureAwait(false):

  • βœ… Library code that doesn't need specific context
  • βœ… ASP.NET Core (no synchronization context by default)
  • βœ… Performance-critical code
  • ❌ UI code that updates UI elements

5. Task.Run vs. async/await πŸƒβ€β™‚οΈ

Task.Run: Queues work to run on the ThreadPool. Use it to offload CPU-bound work.

async/await: For I/O-bound operations (database, file, network). Doesn't use a thread while waiting.

CPU-BOUND vs. I/O-BOUND:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CPU-BOUND (Use Task.Run)                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β€’ Image processing                          β”‚
β”‚ β€’ Complex calculations                      β”‚
β”‚ β€’ Data parsing/transformation               β”‚
β”‚ β€’ Encryption/Decryption                     β”‚
β”‚                                             β”‚
β”‚ Example:                                    β”‚
β”‚ await Task.Run(() => ProcessLargeFile());   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ I/O-BOUND (Use async/await directly)        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β€’ Database queries                          β”‚
β”‚ β€’ HTTP requests                             β”‚
β”‚ β€’ File I/O                                  β”‚
β”‚ β€’ Network operations                        β”‚
β”‚                                             β”‚
β”‚ Example:                                    β”‚
β”‚ await httpClient.GetStringAsync(url);       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. Common Task Combinators πŸ”—

The TPL provides powerful methods to work with multiple Tasks:

+──────────────────────┬──────────────────────────────────────+
β”‚ Method               β”‚ Behavior                             β”‚
+──────────────────────┼──────────────────────────────────────+
β”‚ Task.WhenAll         β”‚ Waits for ALL tasks to complete      β”‚
β”‚ Task.WhenAny         β”‚ Waits for ANY ONE task to complete   β”‚
β”‚ Task.Delay           β”‚ Creates a delay (async version       β”‚
β”‚                      β”‚ of Thread.Sleep)                     β”‚
β”‚ Task.FromResult      β”‚ Creates completed task with result   β”‚
β”‚ Task.CompletedTask   β”‚ Returns already-completed task       β”‚
+──────────────────────┴──────────────────────────────────────+

🧠 Mnemonic Device: "WhenAll" = "Wall" (all tasks must finish, like building a complete wall). "WhenAny" = "Any door" (just need one door to open).


Detailed Examples

Example 1: Basic async/await Pattern πŸ’»

// ❌ SYNCHRONOUS (Blocks thread)
public string GetUserData(int userId)
{
    var client = new HttpClient();
    var response = client.GetStringAsync($"api/users/{userId}").Result; // BLOCKS!
    return response;
}

// βœ… ASYNCHRONOUS (Non-blocking)
public async Task<string> GetUserDataAsync(int userId)
{
    using var client = new HttpClient();
    var response = await client.GetStringAsync($"api/users/{userId}");
    return response; // Automatically wrapped in Task<string>
}

// Usage
public async Task ProcessUserAsync()
{
    var userData = await GetUserDataAsync(123);
    Console.WriteLine($"Received: {userData}");
}

Explanation: The async version allows the thread to be released while waiting for the HTTP response. In a web application, this means the thread can handle other requests instead of sitting idle.

⚠️ Critical Point: Notice we don't need to explicitly create or return a Taskβ€”the async keyword handles that for us. The method returns Task<string>, but we return a plain string.

Example 2: Parallel Operations with Task.WhenAll πŸš€

// Scenario: Fetching data from multiple microservices
public async Task<DashboardData> GetDashboardDataAsync(int userId)
{
    // ❌ SEQUENTIAL (Slow - takes sum of all times)
    var orders = await orderService.GetOrdersAsync(userId);      // 2 seconds
    var recommendations = await recommendationService.GetRecommendationsAsync(userId); // 3 seconds
    var notifications = await notificationService.GetNotificationsAsync(userId);      // 1 second
    // Total time: 6 seconds

    // βœ… PARALLEL (Fast - takes time of slowest)
    var ordersTask = orderService.GetOrdersAsync(userId);
    var recommendationsTask = recommendationService.GetRecommendationsAsync(userId);
    var notificationsTask = notificationService.GetNotificationsAsync(userId);
    
    await Task.WhenAll(ordersTask, recommendationsTask, notificationsTask);
    // Total time: 3 seconds (slowest operation)

    return new DashboardData
    {
        Orders = ordersTask.Result,
        Recommendations = recommendationsTask.Result,
        Notifications = notificationsTask.Result
    };
}

Explanation: By starting all tasks before awaiting any of them, they run concurrently. Task.WhenAll waits for all to complete, but they're executing simultaneously.

SEQUENTIAL EXECUTION:
────────────────────────────────────────────────
Orders         [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ]        (2s)
                        Recommendations [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ]  (3s)
                                                   Notifications [β–ˆβ–ˆβ–ˆ] (1s)
Total: 6 seconds

PARALLEL EXECUTION:
────────────────────────────────────────────────
Orders         [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ]        (2s)
Recommendations[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ]                (3s)
Notifications  [β–ˆβ–ˆβ–ˆ]    (1s)
Total: 3 seconds (limited by slowest)

Example 3: Proper Exception Handling πŸ›‘οΈ

public async Task<UserProfile> GetUserProfileWithRetryAsync(int userId)
{
    int maxRetries = 3;
    int retryCount = 0;
    
    while (retryCount < maxRetries)
    {
        try
        {
            var profile = await userService.GetProfileAsync(userId);
            return profile;
        }
        catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
        {
            retryCount++;
            if (retryCount >= maxRetries)
            {
                throw new Exception($"Failed after {maxRetries} retries", ex);
            }
            
            // Exponential backoff
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
        }
        catch (Exception ex)
        {
            // Log and rethrow non-retryable exceptions
            Console.WriteLine($"Non-retryable error: {ex.Message}");
            throw;
        }
    }
    
    throw new Exception("Unexpected exit from retry loop");
}

Explanation: This pattern demonstrates:

  • Try-catch blocks work naturally with async/await
  • Exception filters (when clause) for specific error handling
  • Task.Delay for async waiting (never use Thread.Sleep in async code!)
  • Exponential backoff strategy (common interview topic)

πŸ’‘ Interview Tip: When discussing exception handling in async code, mention that exceptions are captured in the Task and rethrown when awaited. With Task.WhenAll, if multiple tasks throw exceptions, you get an AggregateException.

Example 4: Cancellation Tokens πŸ›‘

public async Task<List<Product>> SearchProductsAsync(
    string searchTerm, 
    CancellationToken cancellationToken = default)
{
    using var httpClient = new HttpClient();
    
    // Cancellation token is passed through the call chain
    var response = await httpClient.GetAsync(
        $"api/products?search={searchTerm}", 
        cancellationToken);
    
    response.EnsureSuccessStatusCode();
    
    var content = await response.Content.ReadAsStringAsync(cancellationToken);
    return JsonSerializer.Deserialize<List<Product>>(content);
}

// Usage in ASP.NET Core
[HttpGet("products")]
public async Task<IActionResult> GetProducts(
    [FromQuery] string search,
    CancellationToken cancellationToken)
{
    try
    {
        var products = await SearchProductsAsync(search, cancellationToken);
        return Ok(products);
    }
    catch (OperationCanceledException)
    {
        return StatusCode(499, "Client closed request");
    }
}

Explanation: CancellationTokens allow cooperative cancellation of async operations. In ASP.NET Core, the framework automatically provides a token that fires if the client disconnects.

Cancellation Flow:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client       │──→ Sends Request
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€β†’ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚    β”‚ Server starts  β”‚
       β”‚    β”‚ long operation β”‚
       β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚             β”‚
       β”‚  [Client disconnects]
       β”‚             β”‚
       β–Ό             β–Ό
  Cancellation  Operation checks
  Token fires   token periodically
       β”‚             β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
    OperationCanceledException

πŸ”§ Try this: In your next API project, always accept CancellationToken parameters in async methods and pass them through your call chain. It's a best practice that interviewers love to see.


Common Mistakes ⚠️

1. Async Void Methods 🚫

// ❌ BAD: Can't catch exceptions, can't await
public async void ProcessDataAsync()
{
    await SomeOperationAsync();
    // Exception here crashes the app!
}

// βœ… GOOD: Return Task
public async Task ProcessDataAsync()
{
    await SomeOperationAsync();
}

// βœ… ACCEPTABLE: Event handlers only
private async void Button_Click(object sender, EventArgs e)
{
    try
    {
        await ProcessDataAsync();
    }
    catch (Exception ex)
    {
        // Must handle exceptions here
        ShowError(ex.Message);
    }
}

Why it's bad: Async void methods can't be awaited, and exceptions in them can crash your application. The only valid use case is event handlers.

2. Blocking on Async Code (Deadlock Risk) πŸ’€

// ❌ DEADLY: This can deadlock in UI or ASP.NET apps
public string GetUserData(int userId)
{
    var task = GetUserDataAsync(userId);
    return task.Result; // or task.Wait() - both bad!
}

public async Task<string> GetUserDataAsync(int userId)
{
    await dbContext.Users.FindAsync(userId);
    return userData;
}

Why deadlock happens:

  1. .Result blocks the current thread waiting for the task
  2. The awaited task wants to resume on the original context
  3. The original context is blocked by .Result
  4. Deadlock! πŸ’€
Deadlock Scenario:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ UI Thread   │──→ Calls GetUserData()
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜     β”‚
       β”‚            β”‚ task.Result (BLOCKS)
       β”‚            β”‚
       β”‚     GetUserDataAsync starts on ThreadPool
       β”‚            β”‚
       β”‚     await wants to return to UI Thread
       β”‚            β”‚
       β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (Can't! UI Thread is blocked)
       β”‚
    DEADLOCK!

βœ… Solution: Use async all the way, or use ConfigureAwait(false) in libraries.

3. Not Using ConfigureAwait in Libraries πŸ“š

// ❌ In a class library
public async Task<Data> GetDataAsync()
{
    var result = await httpClient.GetAsync(url); // Captures context unnecessarily
    return ProcessResult(result);
}

// βœ… Better performance in libraries
public async Task<Data> GetDataAsync()
{
    var result = await httpClient.GetAsync(url).ConfigureAwait(false);
    return ProcessResult(result);
}

4. Forgetting to Await πŸ€¦β€β™‚οΈ

// ❌ Fire and forget - exceptions are lost!
public void ProcessOrder(Order order)
{
    SaveOrderAsync(order); // Returns Task, but not awaited!
    // Method exits immediately, SaveOrderAsync may not complete
}

// βœ… Properly awaited
public async Task ProcessOrderAsync(Order order)
{
    await SaveOrderAsync(order);
}

5. Using Task.Run for I/O Operations ⚑

// ❌ Wastes a thread
public async Task<string> GetDataAsync()
{
    return await Task.Run(async () => 
    {
        return await httpClient.GetStringAsync(url);
    });
}

// βœ… No unnecessary thread
public async Task<string> GetDataAsync()
{
    return await httpClient.GetStringAsync(url);
}

Why it's wrong: Task.Run takes a ThreadPool thread and makes it wait for the HTTP operation. The HTTP operation is already asyncβ€”you're adding overhead for no benefit.


Key Takeaways 🎯

βœ… Use async/await for I/O-bound operations (database, HTTP, file I/O)

βœ… Use Task.Run for CPU-bound operations (calculations, parsing, image processing)

βœ… Always return Task or Task, never async void (except event handlers)

βœ… Use ConfigureAwait(false) in library code for better performance

βœ… Leverage Task.WhenAll for parallel operations when tasks are independent

βœ… Pass CancellationTokens through your async call chain

βœ… Never block on async code with .Result or .Wait() in UI or ASP.NET apps

βœ… Name async methods with "Async" suffix following convention


πŸ“š Further Study

  1. Microsoft Docs - Async/Await Best Practices: https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

  2. Stephen Cleary's Blog on Async/Await: https://blog.stephencleary.com/2012/02/async-and-await.html

  3. Task-based Asynchronous Pattern (TAP): https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap


πŸ“‹ Quick Reference Card

╔═══════════════════════════════════════════════════════════╗
β•‘          ASYNC/AWAIT QUICK REFERENCE                      β•‘
╠═══════════════════════════════════════════════════════════╣
β•‘ KEYWORDS                                                  β•‘
β•‘ β€’ async β†’ Marks method as asynchronous                    β•‘
β•‘ β€’ await β†’ Suspends execution until Task completes         β•‘
β•‘                                                           β•‘
β•‘ RETURN TYPES                                              β•‘
β•‘ β€’ Task β†’ No return value                                  β•‘
β•‘ β€’ Task<T> β†’ Returns value of type T                       β•‘
β•‘ β€’ void β†’ Event handlers ONLY                              β•‘
β•‘                                                           β•‘
β•‘ TASK COMBINATORS                                          β•‘
β•‘ β€’ Task.WhenAll β†’ Wait for all tasks                       β•‘
β•‘ β€’ Task.WhenAny β†’ Wait for first task                      β•‘
β•‘ β€’ Task.Delay β†’ Async delay (not Thread.Sleep)            β•‘
β•‘                                                           β•‘
β•‘ BEST PRACTICES                                            β•‘
β•‘ β€’ Use ConfigureAwait(false) in libraries                  β•‘
β•‘ β€’ Never use async void (except events)                    β•‘
β•‘ β€’ Don't block with .Result or .Wait()                     β•‘
β•‘ β€’ Pass CancellationTokens through call chain              β•‘
β•‘ β€’ Use Task.Run only for CPU-bound work                    β•‘
β•‘                                                           β•‘
β•‘ COMMON PATTERNS                                           β•‘
β•‘ β€’ I/O-bound β†’ async/await directly                        β•‘
β•‘ β€’ CPU-bound β†’ await Task.Run(() => {...})                 β•‘
β•‘ β€’ Retry logic β†’ await Task.Delay with exponential backoffβ•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

🎯 Interview Pro Tip: When discussing async/await in interviews, always mention the state machine that the compiler generates. It shows you understand what's happening under the hood. Also, be ready to explain the difference between parallelism (Task.Run - multiple threads) and concurrency (async/await - efficient waiting).

Good luck with your interviews! πŸš€