I must be dumb, because every time I dive into async/await, I feel like I reach an epiphany about how it works, and how to use it. Then a week later I read about it again and totally lost all understanding. What do I gain if I have code like this [0], which has a bunch of `.await?` in sequence? I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are…
Nope, async really isn't trivial.
> I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?
It doesn't, that's the charm of it.
It's best to treat 'await' as syntactic sugar, and to dig in to the underlying concepts.
I realise we're not talking C#/.Net, but that's what I know: in .Net, your function might do slow IO (network activity, say) then process the result to produce an int. Your function will have a return-type of `Task`. Your function will quickly return a non-completed Task object, which will enter a completed state only once the network activity has concluded and processing has occurred to give the final `int` value.
The caller of your function can use the `Task#ContinueWith` method, which enqueues work to occur if/when the Task completes, using the result value from the Task. (We'll ignore exceptions here.)
Internal to your function, the network activity itself will also have taken the form of a standard-library Task, and our function will have made use of its `ContinueWith` method. Things can compose nicely in this way; `Task#ContinueWith` returns another Task.
(We needn't think about the particulars of threads too much here, but some thread clearly eventually marks that Task object as completed, so clearly some thread will be in a good position to 'notice' that it's time to act on that `ContinueWith` now. The continuation generally isn't guaranteed to run on the same thread as where we started. That's generally fine, with some notable exceptions.)
You might think that chain-invoking `ContinueWith` would get tedious, as you'd have to write a new function for each step of the way if we make use of several async operations - each continuation means writing another function to pass to `ContinueWith`, after all. Perhaps it would be more natural to just write one big function and have compiler handle the `ContinueWith` calls.
You'd be right. That's why they invented the `await` keyword, which is essentially just syntactic sugar around .Net's `ContinueWith` method. It also correctly handles exceptions, which would otherwise be error-prone, so it's generally best to avoid writing continuations manually.
There's more machinery at play here of course, but that seems like a good starting point.
Assorted related topics:
* If you use `ContinueWith` on a Task which is already completed, it can just stay on the same thread 'here and now' to run your code
* It's possible to produce already-completed Task objects. Rarely useful, but permitted.
* There's plenty going on with thread-pools and .Net 'contexts'
* The often-overlooked possibility of deadlocking if you aren't careful [0]
* None of this would make sense if we had to keep lots of background threads around to fire our continuations, but we don't [1]
* Going async is not the same thing as parallelising, but Tasks are great for managing parallelism too
* This stuff doesn't improve 'straight-line' performance, but it can greatly improve our scalability by avoiding blocking threads to wait on IO. (That is to say, we can better handle a high rate of requests, but our speed at handling a lone request on a quiet day, will be no better.)
I found this overview to be fairly digestible [2]
[0] https://blog.stephencleary.com/2012/07/dont-block-on-async-c...
[1] https://blog.stephencleary.com/2013/11/there-is-no-thread.ht...
[2] https://stackoverflow.com/a/39796872/
See also:
https://docs.microsoft.com/en-us/dotnet/standard/parallel-pr...
https://docs.microsoft.com/en-us/dotnet/api/system.threading...