C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
56: Avoiding Deadlocks in Async Code
You've probably seen it happen: your application just... stops. No crash, no exception, no error message in the logs. The CPU is idling, but the UI is frozen, and the request never completes. This is the classic async deadlock, and it's one of the most frustrating bugs to debug because the code looks perfectly logical on the surface.
The culprit is usually a conflict between the SynchronizationContext—which manages which thread a piece of code runs on—and the way we wait for tasks to complete. Let's build a small user profile loader to see exactly how this happens.
Building a Simple User Profile Loader
I want to create a service that fetches user data from a database. Since I/O operations should be asynchronous, I'll start with a basic repository method. I'm keeping it simple with a Task.Delay to simulate a network call.
public class UserRepository
{
public async Task<string> GetUserNameAsync(int userId)
{
// Simulate a database hit
await Task.Delay(100);
return $"User_{userId}";
}
}
Now, imagine we're working in an environment with a SynchronizationContext (like a WPF app or an older ASP.NET MVC controller). The environment wants to ensure that after an await completes, the execution returns to the original thread—usually the UI thread—so you can safely update the screen.
The Moment Everything Freezes
Here is where I'll make a mistake that I've made dozens of times in my own career. Sometimes, you're forced to call async code from a synchronous method—maybe you're implementing an interface you don't control, or you're just feeling impatient. I'll try to "wrap" the async call to make it look synchronous.
public class UserService
{
private readonly UserRepository _repo = new UserRepository();
public string GetUserName(int userId)
{
// I'll just block the thread until the task is done.
// Seems harmless, right?
return _repo.GetUserNameAsync(userId).Result;
}
}
If you run this in a console app, it might work fine. But in a UI app, this is a death sentence. Here is why:
- The
GetUserNamemethod blocks the main thread while waiting for.Result. - Inside
GetUserNameAsync, theawait Task.Delay(100)finishes. - The runtime tries to jump back to the main thread to finish the method.
- But the main thread is currently blocked, waiting for
.Resultto finish.
They are now staring at each other, both waiting for the other to move. Deadlock.
Breaking the Cycle with Proper Await
The only real cure for this is to "async all the way down." I need to stop blocking the thread and instead let the thread go back to the pool (or the UI loop) until the data is actually ready.
public class UserService
{
private readonly UserRepository _repo = new UserRepository();
// I've changed the signature to be async and returned a Task
public async Task<string> GetUserNameAsync(int userId)
{
// Now we await instead of using .Result
return await _repo.GetUserNameAsync(userId);
}
}
By using await, the main thread is freed up. When the repository finishes its work, the runtime can successfully jump back onto the main thread to finish the UserService method. No more freezing.
When to Use ConfigureAwait(false)
Now, what if you're writing a library? If you're building a NuGet package or a core data layer, you don't actually care which thread the code continues on. You don't need to update a UI label; you just need the data.
In those cases, I always use ConfigureAwait(false). This tells the runtime: "I don't need to return to the original SynchronizationContext. Just finish this on whatever thread is available."
public async Task<string> GetUserNameAsync(int userId)
{
// This prevents the task from trying to jump back to the UI thread,
// making the code more efficient and avoiding deadlocks if the
// caller is blocking.
await Task.Delay(100).ConfigureAwait(false);
return $"User_{userId}";
}
I generally follow this rule of thumb: await` in the UI/Controller layer, ConfigureAwait(false) in the service/library layer. It’s a safety net that prevents your library from causing a deadlock even if the person using your library makes the mistake of calling .Result.
📋 Practical Task
Fixing the Deadlocked ReportGenerator
You have been handed a legacy ReportGenerator class that is causing the application to freeze whenever a report is requested. The developer who wrote this tried to call an asynchronous data fetch synchronously.
Your Task:
Modify the provided code to remove the deadlock. You should apply the "async all the way down" pattern and use ConfigureAwait(false) in the data-fetching layer to ensure the library is deadlock-safe.
public class DataProvider
{
public async Task<string> FetchReportDataAsync()
{
await Task.Delay(500); // Simulate long I/O
return "Report Data Content";
}
}
public class ReportGenerator
{
private readonly DataProvider _provider = new DataProvider();
public string GenerateReport()
{
// BUG: This is causing a deadlock in the UI thread!
var data = _provider.FetchReportDataAsync().Result;
return $"Report: {data}";
}
}There are no comments for now.