The tyranny of async/await

We're now taught to use async/await to create more responsive code, but the reality is that it creates fussy, messy, code bases, and delivers very little benefit.

Back in what is now the mists of time, I wrote a book on Windows 8 development. I wanted to write the book because I was hedging on Windows 8 being a big thing. When I pitched the publisher to do the book, Windows 8 development was “Metro-style”. This ultimately got renamed as first WinRT (Windows Runtime), and is now known as UWP (Universal Windows Platform).

WinRT was obsessed with asynchronous calls. The Microsoft developers reasoned that the way to get the sort of slick user experience that people were getting with iOS and Android was to make everything asynchronous. The framework essentially forbade developers from having access to blocking calls. In order to do support this, the C# language developers added async/await to the language.

The principle of async/await is that we simple humans can look at the code and think it’s single-threaded and blocking, but under the hood the code is broken up and (probably/non-deterministically) executed against separate threads. For example, in this code, GetSomeData can take as it likes to run and callers to DoMagic won’t block. (Ignore that this code won’t work – I’ll get to that.)

private async Foo DoMagic() { var foo = await this.GetSomeData(); // do something based on foo… return foo; }

The problem is that, in C#, the way you do this properly isn’t very light-touch. In reality, you have to ensure that any async method returns back a Task instance, and this Task instance has to wrap the object you want to return. Rewriting, we get this:

private async Task<Foo> DoMagicAsync() { var foo = await this.GetSomeDataAsync(); // do something based on foo… return foo; }

So in C# is that for the benefit of being able to read a routine in a straight line from start to finish, we have to smear this odd Hungarian-notation-style-inspired “Async” at the end of every method, and have everything return tasks. This can lead to signatures like this:

private async Task<Dictionary<string, IEnumerable<FooBarPair>>> GetActivePairsGroupedByAccountCodeAsync(…)

It’s fair to look at that and wonder what the problem is. The issue, for me, is that I’m being obliged to make the code much more unwieldly for little benefit. This approach creates a sort of “two-speed” layer within your code. If you eventually need to “thunk” down to an async call, everything upstream of that has to have this bizarre decoration of returning Task instances, as well as requiring that async keyword. A single async call “back propagates” out into the entire code base.

What’s weird though is that you could achieve the same effect from callbacks, and before you complain, remember that we were doing callbacks in Win16, see EnumWindows et al.

What’s so bad about this:

private void DoMagic(Action<Foo> callback) { this.GetSomeData((foo) => { // do something based on foo… callback(foo); }); }

Answer: Nothing. It’s fine.

(Well, it’s not fine because where do you put exception handling, but let’s assume that we could put that into the language.)

In .NET development, we’re taught not to do that, but if were were using JavaScript (or TypeScript), this code wouldn’t look out of place:

function doMagicAsync(callback) { getSomeData(function(foo) { // do something based on foo… callback(foo); }); }

But than, actually, most of you are now saying: “don’t do that: you should use promises”. This is rather my point. Why are developers obsessed with trying to flatten out what is inherently multi-threaded code? Why do we need to see a simple list of instructions and assume it just goes “a”, “b”, “c”? It doesn’t do that. The routine does that, but the code does not. The code never does and never has. I don’t need my code to be a straightforward flat list of operations in order to maintain it. If I go up out of the routine level to consider the whole software, it’s not flat at that scale. Why impose flatness at a lower scale?

Developers generally understand that computers do multiple things simultaneously via timesharing, and developers also generally understand that some operations take a relatively long amount of time to complete. We know that we have to support asynchronous processing, but who said yet async/await-type constructs were the way to do that. It’s highly artificial.

Now these constructs are expected in all languages. One official Kotlin blog post about coroutines (Kotlin’s equivalent of async/await) says: ‘The great thing about coroutines is that they can suspend without blocking a thread, and yet they look like normal sequential code’. Note my emphasis on “great thing”. Why is it a “great thing”?

I am, I suspect, in a minority here. The problem we have is that now, if you were to actually present code callback-based code when you could be using async/await, or promises, or coroutines, or whatever, you’ll get told that you’re doing it wrong. Yet had we of just started out and said that this callback approach was generally good enough, or even if we’d had said that callback code is straightforwardly representative of what the code is actually doing, that maybe flattening it out is a hack, and that maybe we should just deal with it, we would not be obliged by industry pressure to adopt this approach. We’re going to be doing this for decades now.

This is a shame because, personally, when it comes to C#, async/await turns into some sort of phage that spreads out all over your codebase. We could have added simpler, better features to allow developers to more naturally code in a callback-accepting way, as opposed to how async/await was done.

Share

Our evolving "Digital 247" Software Architecture Methodology helps us guide our technical leadership. We maintain it in two forms — a benchmark that you can fill out to get a feel as to where you are with regards to your peers and competitors, and a 7,000-word downloadable white paper that is free to download and is full of helpful information and advice.

Navigation

Social Media