Stephen's answer is already great, so I'm not going to repeat what he said; I've done my fair share of repeating the same arguments many times on Stack Overflow (and elsewhere).
Instead, let me focus on one important abstract things about asynchronous code: it's not an absolute qualifier. There is no point in saying a piece of code is asynchronous - it's always asynchronous with respect to something else. This is quite important.
The purpose of await
is to build synchronous workflows on top of asynchronous operations and some connecting synchronous code. Your code appears perfectly synchronous1 to the code itself.
var a = await A();
await B(a);
The ordering of events is specified by the await
invocations. B uses the return value of A, which means A must have run before B. The method containing this code has a synchronous workflow, and the two methods A and B are synchronous with respect to each other.
This is very useful, because synchronous workflows are usually easier to think about, and more importantly, a lot of workflows simply are synchronous. If B needs the result of A to run, it must run after A2. If you need to make an HTTP request to get the URL for another HTTP request, you must wait for the first request to complete; it has nothing to do with thread/task scheduling. Perhaps we could call this "inherent synchronicity", apart from "accidental synchronicity" where you force order on things that do not need to be ordered.
You say:
In my mind, since I do mainly UI dev, async code is code that does not run on the UI thread, but on some other thread.
You're describing code that runs asynchronously with respect to the UI. That is certainly a very useful case for asynchrony (people don't like UI that stops responding). But it's just a specific case of a more general principle - allowing things to happen out of order with respect to one another. Again, it's not an absolute - you want some events to happen out of order (say, when the user drags the window or the progress bar changes, the window should still redraw), while others must not happen out of order (the Process button must not be clicked before the Load action finishes). await
in this use case isn't that different from using Application.DoEvents
in principle - it introduces many of the same problems and benefits.
This is also the part where the original quote gets interesting. The UI needs a thread to be updated. That thread invokes an event handler, which may be using await
. Does it mean that the line where await
is used will allow the UI to update itself in response to user input? No.
First, you need to understand that await
uses its argument, just as if it were a method call. In my sample, A
must have already been invoked before the code generated by await
can do anything, including "releasing control back to the UI loop". The return value of A
is Task<T>
instead of just T
, representing a "possible value in the future" - and await
-generated code checks to see if the value is already there (in which case it just continues on the same thread) or not (which means we get to release the thread back to the UI loop). But in either case, the Task<T>
value itself must have been returned from A
.
Consider this implementation:
public async Task<int> A()
{
Thread.Sleep(1000);
return 42;
}
The caller needs A
to return a value (a task of int); since there's no await
s in the method, that means the return 42;
. But that cannot happen before the sleep finishes, because the two operations are synchronous with respect to the thread. The caller thread will be blocked for a second, regardless of whether it uses await
or not - the blocking is in A()
itself, not await theTaskResultOfA
.
In contrast, consider this:
public async Task<int> A()
{
await Task.Delay(1000);
return 42;
}
As soon as the execution gets to the await
, it sees that the task being awaited isn't finished yet and returns control back to its caller; and the await
in the caller consequently returns control back to its caller. We've managed to make some of the code asynchronous with respect to the UI. The synchronicity between the UI thread and A was accidental, and we removed it.
The important part here is: there's no way to distinguish between the two implementations from the outside without inspecting the code. Only the return type is part of the method signature - it doesn't say the method will execute asynchronously, only that it may. This may be for any number of good reasons, so there's no point in fighting it - for example, there's no point in breaking the thread of execution when the result is already available:
var responseTask = GetAsync("http://www.google.com");
// Do some CPU intensive task
ComputeAllTheFuzz();
response = await responseTask;
We need to do some work. Some events can run asynchronously with respect to others (in this case, ComputeAllTheFuzz
is independent of the HTTP request) and are asynchronous. But at some point, we need to get back to a synchronous workflow (for example, something that requires both the result of ComputeAllTheFuzz
and the HTTP request). That's the await
point, which synchronizes the execution again (if you had multiple asynchronous workflows, you'd use something like Task.WhenAll
). However, if the HTTP request managed to complete before the computation, there's no point in releasing control at the await
point - we can simply continue on the same thread. There's been no waste of the CPU - no blocking of the thread; it does useful CPU work. But we didn't give any opportunity for the UI to update.
This is of course why this pattern is usually avoided in more general asynchronous methods. It is useful for some uses of asynchronous code (avoiding wasting threads and CPU time), but not others (keeping the UI responsive). If you expect such a method to keep the UI responsive, you're not going to be happy with the result. But if you use it as part of a web service, for example, it will work great - the focus there is on avoiding wasting threads, not keeping the UI responsive (that's already provided by asynchronously invoking the service endpoint - there's no benefit from doing the same thing again on the service side).
In short, await
allows you to write code that is asynchronous with respect to its caller. It doesn't invoke a magical power of asynchronicity, it isn't asynchronous with respect to everything, it doesn't prevent you from using the CPU or blocking threads. It just gives you the tools to easily make a synchronous workflow out of asynchronous operations, and present part of the whole workflow as asynchronous with respect to its caller.
Let's consider an UI event handler. If the individual asynchronous operations happen to not need a thread to execute (e.g. asynchronous I/O), part of the asynchronous method may allow other code to execute on the original thread (and the UI stays responsive in those parts). When the operation needs the CPU/thread again, it may or may not require the original thread to continue the work. If it does, the UI will be blocked again for the duration of the CPU work; if it doesn't (the awaiter specifies this using ConfigureAwait(false)
), the UI code will run in parallel. Assuming there's enough resources to handle both, of course. If you need the UI to stay responsive at all times, you cannot use the UI thread for any execution long enough to be noticeable - even if that means you have to wrap an unreliable "usually asynchronous, but sometimes blocks for a few seconds" async method in a Task.Run
. There's costs and benefits to both approaches - it's a trade-off, as with all engineering :)
- Of course, perfect as far as the abstraction holds - every abstraction leaks, and there's plenty of leaks in await and other approaches to asynchronous execution.
- A sufficiently smart optimizer might allow some part of B to run, up to the point where the return value of A is actually needed; this is what your CPU does with normal "synchronous" code (Out of order execution). Such optimizations must preserve the appearance of synchronicity, though - if the CPU misjudges the ordering of operations, it must discard the results and present a correct ordering.