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 valueTask<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>, orvoid(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 (
whenclause) for specific error handling Task.Delayfor async waiting (never useThread.Sleepin 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:
.Resultblocks the current thread waiting for the task- The awaited task wants to resume on the original context
- The original context is blocked by
.Result - 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
β 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
Microsoft Docs - Async/Await Best Practices: https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
Stephen Cleary's Blog on Async/Await: https://blog.stephencleary.com/2012/02/async-and-await.html
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! π