Useful Links
- David Fowler’s Async Guidance
- Understanding the Whys, Whats, and Whens of ValueTask
- Why you should use ConfigureAwait(false) in your library code
- Correcting Common Async/Await Mistakes in .NET
- What is Synchronization Context?
- ConfigureAwait FAQ
- The Managed Thread Pool
- Do I need to dispose of Tasks?
- MSDN Magazine: Synchronization Context
- ASP.NET Core Synchronization Context
Table of Contents
- Async/Await Basics
- Making Async Code Faster
- Common Refactors
- Awaiting completed and noncompleted tasks example
- ConfigureAwait(false)
- Awaiting vs Returning a Task
- ValueTask
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:
- For I/O-bound code, you await an operation that returns a
Task
orTask<T>
inside of anasync
method. - For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.
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
- Get rid of .Wait()
- Can’t use await
- Adding ConfigureAwait(false)
- Returning Task instead of awaiting
- Where you shouldn’t return Task directly
- Using ValueTask
- Async in constructor (async void)
- Async in ICommand (MVVM)
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:
- The async method doesn’t return when awaiting the completed task; the method keeps executing synchronously.
- The async method does return when awaiting the delay task. That’s why the third line is
Method returned
, printed in theMain
method. The async method can tell that the operation it’s waiting for (the delay task) hasn’t completed yet, so it returns to avoid blocking. - The task returned from the async method completes only when the method completes. That’s why
Task completed
is printed afterAfter second await
.
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
:
- Asynchronous and synchronous exceptions are normalized to always be asynchronous.
- The code is easier to modify (consider adding a
using
, for example). - Diagnostics of asynchronous methods are easier (debugging hangs etc).
- Exceptions thrown will be automatically wrapped in the returned
Task
instead of surprising the caller with an actual exception. - Async locals will not leak out of async methods. If you set an async local in a non-async method, it will “leak” out of that call.
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:
- Await a
Task
multiple times - Store
Task
s in a dictionary for any number of subsequent consumers to await in the future (cache for async results) - Block waiting for one to complete
- Consume a large variety of operations over tasks (“When All” or “When Any”)
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>
:
- Awaiting a ValueTask multiple times. The underlying object my have been recycled already.
- Awaiting a ValueTask concurrently. The underlying object expects to work with only a single callback from a single consumer at a time. Disobeying this can lead to race conditions.
- Using .GetAwaiter().GetResult() when the operation hasn’t yet completed. This is inherently a race condition.
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:
- Only Consume Once
- 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.