How and when to use ‘async’ and ‘await’
When using async
and await
the compiler generates a state machine in the background.
Here's an example on which I hope I can explain some of the high-level details that are going on:
public async Task MyMethodAsync()
{
Task<int> longRunningTask = LongRunningOperationAsync();
// independent work which doesn't need the result of LongRunningOperationAsync can be done here
//and now we call await on the task
int result = await longRunningTask;
//use the result
Console.WriteLine(result);
}
public async Task<int> LongRunningOperationAsync() // assume we return an int from this long running operation
{
await Task.Delay(1000); // 1 second delay
return 1;
}
OK, so what happens here:
Task<int> longRunningTask = LongRunningOperationAsync();
starts executingLongRunningOperation
Independent work is done on let's assume the Main Thread (Thread ID = 1) then
await longRunningTask
is reached.Now, if the
longRunningTask
hasn't finished and it is still running,MyMethodAsync()
will return to its calling method, thus the main thread doesn't get blocked. When thelongRunningTask
is done then a thread from the ThreadPool (can be any thread) will return toMyMethodAsync()
in its previous context and continue execution (in this case printing the result to the console).
A second case would be that the longRunningTask
has already finished its execution and the result is available. When reaching the await longRunningTask
we already have the result so the code will continue executing on the very same thread. (in this case printing result to console). Of course this is not the case for the above example, where there's a Task.Delay(1000)
involved.
From my understanding one of the main things that async and await do is to make code easy to write and read.
They're to make asynchronous code easy to write and read, yes.
Is it the same thing as spawning background threads to perform long duration logic?
Not at all.
// I don't understand why this method must be marked as 'async'.
The async
keyword enables the await
keyword. So any method using await
must be marked async
.
// This line is reached after the 5 seconds sleep from DoSomethingAsync() method. Shouldn't it be reached immediately?
No, because async
methods are not run on another thread by default.
// Is this executed on a background thread?
No.
You may find my async
/await
intro helpful. The official MSDN docs are also unusually good (particularly the TAP section), and the async
team put out an excellent FAQ.
Explanation
Here is a quick example of async
/await
at a high level. There are a lot more details to consider beyond this.
Note: Task.Delay(1000)
simulates doing work for 1 second. I think it's best to think of this as waiting for a response from an external resource. Since our code is waiting for a response, the system can set the running task off to the side and come back to it once it's finished. Meanwhile, it can do some other work on that thread.
In the example below, the first block is doing exactly that. It starts all the tasks immediately (the Task.Delay
lines) and sets them off to the side. The code will pause on the await a
line until the 1 second delay is done before going to the next line. Since b
, c
, d
, and e
all started executing at almost the exact same time as a
(due to lack of the await), they should finish at roughly the same time in this case.
In the example below, the second block is starting a task and waiting for it to finish (that is what await
does) before starting the subsequent tasks. Each iteration of this takes 1 second. The await
is pausing the program and waiting for the result before continuing. This is the main difference between the first and second blocks.
Example
Console.WriteLine(DateTime.Now);
// This block takes 1 second to run because all
// 5 tasks are running simultaneously
{
var a = Task.Delay(1000);
var b = Task.Delay(1000);
var c = Task.Delay(1000);
var d = Task.Delay(1000);
var e = Task.Delay(1000);
await a;
await b;
await c;
await d;
await e;
}
Console.WriteLine(DateTime.Now);
// This block takes 5 seconds to run because each "await"
// pauses the code until the task finishes
{
await Task.Delay(1000);
await Task.Delay(1000);
await Task.Delay(1000);
await Task.Delay(1000);
await Task.Delay(1000);
}
Console.WriteLine(DateTime.Now);
OUTPUT:
5/24/2017 2:22:50 PM
5/24/2017 2:22:51 PM (First block took 1 second)
5/24/2017 2:22:56 PM (Second block took 5 seconds)
Extra info regarding SynchronizationContext
Note: This is where things get a little foggy for me, so if I'm wrong on anything, please correct me and I will update the answer. It's important to have a basic understanding of how this works but you can get by without being an expert on it as long as you never use ConfigureAwait(false)
, although you will likely lose out on some opportunity for optimization, I assume.
There is one aspect of this which makes the async
/await
concept somewhat trickier to grasp. That's the fact that in this example, this is all happening on the same thread (or at least what appears to be the same thread in regards to its SynchronizationContext
). By default, await
will restore the synchronization context of the original thread that it was running on. For example, in ASP.NET you have an HttpContext
which is tied to a thread when a request comes in. This context contains things specific to the original Http request such as the original Request object which has things like language, IP address, headers, etc. If you switch threads halfway through processing something, you could potentially end up trying to pull information out of this object on a different HttpContext
which could be disastrous. If you know you won't be using the context for anything, you can choose to "not care" about it. This basically allows your code to run on a separate thread without bringing the context around with it.
How do you achieve this? By default, the await a;
code actually is making an assumption that you DO want to capture and restore the context:
await a; //Same as the line below
await a.ConfigureAwait(true);
If you want to allow the main code to continue on a new thread without the original context, you simply use false instead of true so it knows it doesn't need to restore the context.
await a.ConfigureAwait(false);
After the program is done being paused, it will continue potentially on an entirely different thread with a different context. This is where the performance improvement would come from -- it could continue on on any available thread without having to restore the original context it started with.
Is this stuff confusing? Hell yeah! Can you figure it out? Probably! Once you have a grasp of the concepts, then move on to Stephen Cleary's explanations which tend to be geared more toward someone with a technical understanding of async
/await
already.