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 type T. 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