Asynchronous Programming in C#

27 Aug 2022

← Home
9 min. read

Table of Contents

Async/Await Basics

Overview of the asynchronous model

The core of async programming is the Task and Task<T> objects, which model asynchronous operations. They are supported by the async and await keywords. The model is fairly simple in most cases:

The await keyword is where the magic happens. It yields control to the caller of the method that performed await, and it ultimately allows a UI to be responsive or a service to be elastic.

I/O-bound example

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

CPU-bound example

private DamageResult CalculateDamageDone()
{
    // Expensive/long-running calculation
}

calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};
But I was told Task.Run is bad?!

If the work you have is I/O-bound, use async and await without Task.Run. You should not use the Task Parallel Library.

If the work you have is CPU-bound and you care about responsiveness, use async and await, but spawn off the work on another thread with Task.Run. If the work is appropriate for concurrency and parallelism, also consider using the Task Parallel Library. (The TPL is designed well, but writing robust and readable code can be a challenge.)

Waiting for multiple tasks to complete

You can use Task.WhenAll and Task.WhenAny. They allow you to write asynchronous code that performs a non-blocking wait on multiple background tasks.

public async Task<User> GetUserAsync(int userId)
{
    // Gets the user from the database where its id = userId
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

The async void case

While an async method can return void. You should only do this in the case of Event Handlers.

When an exception is thrown within an async method, the exception is attached to the returned task to be handled later. But when an exception is thrown within an async void method, the exception is thrown in the current SynchronizationContext, which means your catch block won’t catch the exception.

Also, it’s easy to determine when an async Task method has completed, but not so for an async void method.

Beware of passing async lambdas into a method as an Action parameter; the async lambda returns void and inherits all the problems of async void methods. Async lambdas should only be used if they’re converted to a delegate type that returns Task (Func<Task>).

Making Async Code Faster

BEFORE

private async Task SomeMethod()
{
    var httpClient = new HttpClient();

    // starts each Task one at a time
    var youtubeSubs = await GetYoutubeSubs(httpClient);
    var twitterFollowers = await GetTwitterFollower(httpClient);
    var githubFollowers = await GetGithubFollowers(httpClient);

    // ...
}

AFTER

private async Task SomeMethod()
{
    var httpClient = new HttpClient();

    var youtubeSubsTask = GetYoutubeSubs(httpClient);
    var twitterFollowersTask = GetTwitterFollower(httpClient);
    var githubFollowersTask = GetGithubFollowers(httpClient);

    // fires all Tasks at once
    await Task.WhenAll(youtubeSubsTask, twitterFollowersTask, githubFollowersTask);

    // Then access the data with .Result
    // (no issues to do this, because all these tasks have completed from awaiting the WhenAll)
    var youtubeSubs = youtubeSubsTask.Result;
    var twitterFollowers = twitterFollowersTask.Result;
    var githubFollowers = githubFollowersTask.Result;

    // ...
}

Common Refactors

Awaiting completed and noncompleted tasks example

This example is from C# in Depth:

static void Main()
{
    Task task = DemoCompletedAsync(); // Calls the async method
    Console.WriteLine("Method returned");
    task.Wait(); // Blocks until the task completes
    Console.WriteLine("Task completed");
}

static async Task DemoCompletedAsync()
{
    Console.WriteLine("Before first await");
    await Task.FromResult(10); // Awaits a completed task
    Console.WriteLine("Between awaits");
    await Task.Delay(1000); // Awaits a noncompleted task
    console.WriteLine("After second await.");
}

The output from the above is as follows:

Before first await
Between awaits
Method returned
After second await
Task completed

Important aspects:

ConfigureAwait(false)

By default, when an async operation finishes, the compiler gets the thread that spawned that async method to finish the remaining computations. This is desired behavior when code interacts with the UI.

But when the code doesn’t interact with the UI, we’d rather that any available thread come to finish the remaining work. That behavior can be achieved by appending .ConfigureAwait(false) to an awaited operation.

For example:

// if thread 3 spawns a new thread to work on this async method... (let's say thread 15)
var topStoryIds = await GetTopStoryIds();

// then thread 3 must return to perform this code when thread 15 finishes GetTopStoryIds()
for (int i = 0; i < numberOfStores; i++)
{
    var story = await GetStory(topStoryIds[i])
    topStoryList.Add(story);
}

But when we add .ConfigureAwait(false):

// thread 3 still spawns another thread
var topStoryIds = await GetTopStoryIds().ConfigureAwait(false);

// but now, since we used ConfigureAwait(false), any available thread can perform the restf
for (int i = 0; i < numberOfStores; i++)
{
    var story = await GetStory(topStoryIds[i]).ConfigureAwait(false);
    topStoryList.Add(story);
}

You Don’t Need ConfigureAwait(false) for ASP.NET Core apps

Excerpt from Stephen Cleary’s ASP.NET Core SynchronizationContext:

Since there is no context anymore, there’s no need for ConfigureAwait(false). Any code that knows it’s running under ASP.NET Core does not need to explicitly avoid its context. In fact, the ASP.NET Core team themselves have dropped the use of ConfigureAwait(false).

However, I still recommend that you use it in your core libraries - anything that may be reused in other applications. If you have code in a library that may also run in a UI app, or legacy ASP.NET app, or anywhere else there may be a context, then you should still use ConfigureAwait(false) in that library.

Awaiting vs Returning a Task

There are benefits to using the async/await keyword instead of directly returning the Task:

BAD

public Task<int> DoSomethingAsync()
{
    return CallDependencyAsync();
}

GOOD

public async Task<int> DoSomethingAsync()
{
    return await CallDependencyAsync();
}

ValueTask

Task as a class is very flexible and has resulting benefits:

These benefits make Tasks and async/await so handy in .NET, but sometimes those features become unnecessary overhead.

Suppose we have:

TResult result = await SomeOperationAsync();
UseResult(result)

In this example, we don’t need any of the above benefits, because we’re using the result as soon as await calls back.

Now, suppose we have a method like so:

public async Task WriteAsync(byte value)
{
    if (_bufferedCount == _buffer.Length)
    {
        await FlushAsync();
    }
    _buffer[_bufferedCount++] = value;
}

We can expect that in most cases, this buffer will not be full, and therefore will not call FlushAsync(). But since it returns the non-generic Task, the runtime will cache a single non-generic Task and use that over and over again each time that this method completes its synchronous path.

For this example:

public async Task<bool> MoveNextAsync()
{
    if (_bufferedCount == 0)
    {
        await FillBuffer();
    }
    return _bufferedCount > 0;
}

The runtime will cache two Task<bool>, one with result true, and the other with result false. The runtime maintains a small cache for other types as well, but it’s not feasible to cache everything. (think Int32 and all the possible results)

ValueTask solves the issue of unnecessary allocations when an async method runs synchronously.

Valid consumption patterns for ValueTasks

These operations should NEVER be performed on ValueTask/ValueTask<TResult>:

If you have a ValueTask and need to do one of the above, you should use .AsTask() to get a Task. Then NEVER interact with that ValueTask again.

TL/DR:

  1. Only Consume Once
  2. Only Consume Asynchronously

THEN NEVER USE IT AGAIN

Examples of good and bad practices:

// Given this ValueTask<int>-returning method…
public ValueTask<int> SomeValueTaskReturningMethodAsync();
…
// GOOD
int result = await SomeValueTaskReturningMethodAsync();

// GOOD
int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false);

// GOOD
Task<int> t = SomeValueTaskReturningMethodAsync().AsTask();

// WARNING
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
... // storing the instance into a local makes it much more likely it'll be misused,
    // but it could still be ok

// BAD: awaits multiple times
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = await vt;
int result2 = await vt;

// BAD: awaits concurrently (and, by definition then, multiple times)
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
Task.Run(async () => await vt);
Task.Run(async () => await vt);

// BAD: uses GetAwaiter().GetResult() when it's not known to be done
ValueTask<int> vt = SomeValueTaskReturningMethodAsync();
int result = vt.GetAwaiter().GetResult();

Should every async API return ValueTask?

No, because it’s easy to use ValueTask incorrectly. However, in cases where substantial performance improvements can be made, ValueTask should be considered.