Asynchronous Programming
Asynchronous programming is crucial for maintaining the responsiveness and efficiency of applications, especially in environments where tasks may be time-consuming or involve waiting for external resources. By allowing your application to perform other work while waiting for long-running operations to complete, you can improve both user experience and system performance.
Imagine if you went to google.com in your browser and the whole app locked up while you downloaded the data and rendered it. I don't think you'd be using that browser for very long.
Async programming allows us to free up resources (threads, usually) while we wait for typically I/O bound operations (like reading byte streams or waiting for a database to return with data) to complete.
Benefits
- Improved User Experience: Asynchronous programming helps keep your application responsive by performing tasks in the background and avoiding freezing or blocking the user interface.
- Efficient Resource Utilization: It allows better utilization of system resources by not blocking threads while waiting for operations to complete. In web frameworks like ASP.NET, this can free up a thread to handle another request while the current one is waiting for a response and is a critical component for its ability to scale.
Overview of async
/await
The async
and await
keywords in C# provide a simple way to write asynchronous code. They allow you to perform asynchronous operations without explicitly managing threads or using callbacks.
Example
Here’s a basic example demonstrating the use of async
and await
:
public async Task<string> GetDataFromServerAsync()
{
var client = new HttpClient();
string result = await client.GetStringAsync("https://example.com");
return result;
}
Task
and Task<T>
Task
: Represents an ongoing operation that does not return a value. It is used for methods that perform asynchronous operations but do not need to return a result.Task<T>
: Represents an ongoing operation that returns a value of typeT
. It is used for methods that perform asynchronous operations and return a result.
Do’s
Async All the Way Down
If a method calls asynchronous APIs, it should itself be declared as async. This ensures that the calling code can also take advantage of asynchronous operations. For most modern .NET code, this isn't too problematic.
public async Task ProcessDataAsync()
{
var data = await GetDataFromServerAsync();
// Process data
}
That said, don't declare EVERY method as async. Only declare a method as async if it actually performs asynchronous operations. Declaring synchronous methods as async can lead to unnecessary overhead.
Favor Async APIs
Use .NET’s Async APIs: Prefer using asynchronous methods provided by .NET over blocking calls. This helps in achieving better scalability and responsiveness.
Example: .NET provides an async
version of Task.Delay
that you should use instead of Thread.Sleep
.
// Bad
Thread.Sleep(1000);
// Good
await Task.Delay(1000);
Another example, deserializing JSON using System.Text.Json
:
// Not ideal
string json = File.ReadAllText("data.json");
var settings = JsonSerializer.Deserialize<Settings>(json);
// Good
using Stream jsonStream = File.OpenRead("data.json");
var settings = await JsonSerializer.DeserializeAsync<Settings>(jsonStream);
Don’t’s
Don’t Use .Result
or .Wait()
Avoid Synchronous Blocking: Using .Result or .Wait() on a task can lead to deadlocks, especially in older .NET applications with a synchronization context. (It's not important for this course to understand synchronization context deeply - just know for now to avoid using .Result
or .Wait()
.)
// Problematic code
var result = GetDataFromServerAsync().Result; // Can cause deadlocks
Don’t Declare Every Method as Async
Avoid Overusing Async: Only declare a method as async if it actually performs asynchronous operations. Declaring synchronous methods as async can lead to unnecessary overhead.
Avoid async void
Use async Task Instead: async void
methods are intended for legacy event handlers and should not be used for other methods. They do not return a Task and are harder to manage and test.
// This is about the only time you'll see async void
public async void HandleButtonClick(object sender, EventArgs e)
{
// Async code
}
🌶️🌶️🌶️ Every time someone writes async void
outside the context of an event handler, Jon Skeet himself summons tainted souls into the realm of the living. If you write async void
you are giving in to Them and their blasphemous ways which doom us all to inhuman toil for the One whose Name cannot be expressed in the Basic Multilingual Plane, he comes.
Don’t Wrap Synchronous Methods with Task.Start
Avoid Wrapping Synchronous Code: Wrapping synchronous methods in Task.Start is not an effective way to achieve asynchronous behavior and can lead to inefficiencies. Remember, async
all the way down!
// Problematic code
Task.Run(() => SynchronousMethod()); // Inefficient and incorrect