Async Wandering Part 8 — async and await — the biggest C# mistake?

This is the eighth part of the Async Wandering series. For your convenience you can find other parts in the table of contents in Part 1 – Why creating Form from WinForms in unit tests breaks async?

Async is one of the most popular C# features nowadays. It changes the way we write applications and gives a lot of advantages. However, it has a lot of significant drawbacks which we tend to ignore. In this post I’ll talk about some of them.

It’s expensive!

State machine cost

Every single time you mark your method as async C# compiler does a lot of magic. It introduces new type, does some bookkeeping for the machine, handles exceptions. While these things seem to be harmless, they do pile up. If your whole API is async-based then introducing state machine at every level will become expensive.

This may be very bad in high performance code. Actually, one of the first things you do when you want to improve the performance is you get rid of the state machine, which is against Microsoft guidelines

Garbage collection

Each task is allocated on the heap. This takes memory and requires Garbage Collector to do additional work. Even though it doesn’t have finalizer, it does manage state which is released when GC cleans up the objects. See Part 7 for more details.

Because of the GC pressure, C# introduced ValueTask which is stack allocated. However, because of issues I’ll discuss later, transition between the two is not that easy.

It doesn’t integrate with rest of the platform that nicely

async all the way up

When you use async you need to use it all the way up in your code. If you try waiting for a task synchronously, you’ll most likely end up with a deadlock. Consider the code below:

If you try running this code in a console application — it works. If you use this code in a desktop app (WPF for instance) it hangs forever. Why? That’s because you synchronously wait for a task and because it uses global state under the hood, it cannot post a continuation on the same thread where it started because that thread is blocked. It is not a problem in a console app since it works differently there (continue reading to understand why).

ConfigureAwait(false) all the way down

async all the way up!” is a common phrase. However, it’s not the end of the story. Take this code:

Again, this hangs in UI applications. Why? We do ConfigureAwait in operationOnContext so continuation after awaiting should run on the thread pool right?

That’s right but there are more continuations here. operationOnContext2 awaits another method but doesn’t use ConfigureAwait so it captures the context. It doesn’t matter that one level above method doesn’t do it, context can be captured anywhere. Again, after waiting the continuation wants to return to the UI thread which is blocked.

void

Using async void methods is a very bad practice. But we need to do that sometimes! First, you have a lot of UI frameworks using handlers which expect you to provide a void method. However, delegates are worse — you cannot return a value from a delegate reliably. Some languages don’t support that at all (VB.NET) so the code is not portable anymore. What’s more, only last value from the delegate is returned — if you had multiple values returned then most of them are lost. So you cannot await the event easily. It is doable as in Part 4 but it is a very hacky solution.

void in disguise

void is not necessarily about returning nothing from the function, it is about not returning the Task as expected. There is one more place where we can’t use tasks easily — constructor. This collides with good OOP design when you want to have some checks when creating the object but they require going to database/network/etc.
There are other places where you can’t use tasks easily, some of them are fixed by C# language over time. Disposable items, foreach loops, etc. They show that async wasn’t integrated properly since the very beginning.

Exceptions

async changes exception behavior. First, it automatically captures all the exceptions and stores them on the Task object which may result in exception going unnoticed. See Part 7 for more details.

But it also changes behavior of aggregated exceptions, as shown in Part 6. You can await tasks but they won’t report all the exceptions as they would normally. Again, exceptions may go unnoticed because of that.

It doesn’t work with synchronization primivites

What happens if you try awaiting inside lock like here?

We get this error:

And it makes perfect sense. If we await we may continue on other thread and we can never stop that. How would we move the lock to the continuation thread? Well, we could do some magic on the CLR level but then it wouldn’t work with OS primitives anyway (unless we integrate .NET with the system).
There is another issue — await releases the thread so it can do something else. Should the lock still be held? If we do so then we risk another deadlock if the thread runs code trying to lock on the same mechanism. Should we release the lock? Well, that would be even worse because critical section wouldn’t be protected anymore.
So it looks like everything is perfect? Not so fast. We know how lock is implemented in C# so why not unrolling it manually like here?

It compiles and prints the following:

So we switch the thread after waiting and then monitor is not held. But you know what? The monitor is still taken by the thread 1. So we caused yet another deadlock. This will behave the same way with any other synchronization primitive, managed or not.

It can change thread anytime

What else is broken? Basically anything depending on the thread. Do you have thread static? It won’t work. Do you tie permissions to a thread? Most likely broken. Thread priorities?

This impacts other things as well — what about call stacks? It’s not simple to retrieve the trace easily, especially when things are moved to the heap.

It uses hidden state

Synchronization context capture

Wherever you await a task, you capture the synchronization context by default. This is why some code may work in a console application but may break in a UI one. There are multiple synchronization contexts and they differ significantly.

In console based application there is no synchronization context. In that situation, await doesn’t capture anything (cause there is nothing to capture) and uses a thread pool. Later on, when continuation is passed to run, it goes through the task scheduler and ends up on a thread pool so it has some thread to continue on.

In UI applications there is a synchronization context which uses exactly one thread – the UI thread. Whenever you post a continuation, it must be executed on that one thread. If it is blocked — you are out of luck and get a deadlock. See Part 3 how to fix that in UI applications.

What’s worse, the library you use may decide to not capture a synchronization context and change the behavior of your application (by running on a thread pool). Also, when it comes to Microsoft guidelines — at the time of writing this post there is none. However, most people (myself included) recommend calling ConfigureAwait(false) always unless you know what you’re doing.

Context on a thread

Things get even worse. In previous section I wrote that in console app there is no synchronization context. But this thing doesn’t depend on the application type — it can be changed in runtime. This is effectively a global state which anyone can modify. For instance, see Part 1 how UI frameworks modify the context on initialization. This is something which can easily go unnoticed and you have absolutely no control on that.

It breaks (or at least doesn’t follow) best practices

Endless waiting

First rule of waiting in concurrent scenarios is always wait with a timeout. It’s not easy to choose correct value, it depends on the application, CPU usage, peak/non-peak time and many more details. However, you should always have some timeout at least, so when your application hangs it can heal itself automatically.

How do you specify timeout using await? You probably don’t want to add any logic in the callee as it shouldn’t care. So you either wrap the call site and you end up with something like await Foo().SetTimeout(123).ConfigureAwait(false) (where SetTimeout is a nice extension method) or you modify synchronization context or you introduce new task type with AsyncMethodBuilder (if you have no idea — read fantastic post by Kevin Gosse).

First solution makes your codebase even more polluted (and it’s already pretty bad with ConfigureAwait calls) not to mention that one day you’ll find a place where you forgot to call the method. You can obviously use Fody or other nice tricks to modify your code on the fly but that’s risky.

Second solution won’t work unless you rewrite a lot. You probably don’t want to mess with synchronization context (especially not in UI apps) not to mention that it doesn’t need to be captured. You can rewrite thread pools, create custom schedulers and apply context automatically but this is magical again.

Third solution seems pretty nice, however, once your new task is casted to good old Task you’re done.

DRY — Synchronous and asynchronous methods

What happens if you want to expose synchronous and asynchronous version of your API? Well, you just create two methods. See File.ReadAllText and similar methods. They all have async counterpart. Imagine now, what may happen in few years if C# introduces another task type. Yet another method, just to support new use case.

And it’s not that async method just calls synchronous one under the hood. This is synchronous path and this is asynchronous one. When I’m writing this post, they look like this:

Synchronous:

Asynchronous:

This is inextensible and doesn’t allow the old code to use new features. And we already have two task types (Task and ValueTask) so it is not that unlikely to get yet another one in future.

Dependency Inversion principle

The biggest issue is it breaks the Dependency Inversion principle. The principle says:

1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

Digression: every time you use concrete implementation instead of an interface you break this principle. Have you ever thought about strings? Is using System.String correct? And before you answer “nobody is going to rewrite string” let me just point you to some cases where people implement their own strings.

Now each async method in your interface depends on concrete implementation which is Task. Since it also relies on global state, you effectively cannot control how it works. What if you’d like to run it on a constrained thread pool? What if use only one thread? What if you’d like to wait for all possible continuations generated by given flow? No way, cannot be done because Task uses synchronization context and a thread pool internally, and these can be changed beyond your control.

So how should we fix this? Well, creating new APIs with another parameter like IThreadPool won’t work. It breaks DRY and the parameter may be ignored. What should be used instead is a special type controlling the behavior. And functional programming already has a type for that — it’s called Monad.

I’m not going to explain monads here (there are tons of articles out there). Monads capture values and put them in context, and this is exactly what we want with async. The same way monad may have a value or not (Maybe/Option), exactly one value (Id), or multiple values (List) — the same way the context here is the asynchronous execution. See the code below:

We start with very simple abstraction:

Now, we implement a context with exactly one value:

What does this do? Well, it just wraps a value in a no-op context. It’s like a boxed value type, or a nullable. We can see that it doesn’t do anything special, it just holds the value and performs an operation on it.

Now, let’s do this:

So we start with a context holding a file name, then we read the file and return a context holding the content. Let’s run the code and see:

Okay, so we see our content and we see that the thread is the same before, during and after reading. Also, notice the delay between first two lines — because we run everything synchronously, the sleep is there. Now, what if we wanted to introduce asynchronous execution?

Our new context holds a value in Task.FromResult, so you can see this is nothing new. However, when we bind the operation, we use ContinueWith method to execute operation in the context. Let’s now do this:

Notice, this is exactly the same code as before. What we actually change is we replace

with

See the output now:

What happened? We introduced asynchronous execution and didn’t await the task. Let’s add some sleep at the end and then the output is:

Notice that the delay is now between lines 2 and 3. That’s because we wrapped sleep in ContinueWith so it didn’t block the main thread.

The important part here is: we encapsulated details of execution (synchronous vs asynchronous) in the type, not in the internals of the Download method. This means that the method and any infrastructure under the hood don’t need to know about thread pools, tasks, synchronization contexts or whatever else. They don’t care — they just create a series of transformations.

Now, in your production code you can use whatever thread pool you like, and in your unit tests you can go with synchronous execution. All with just one line of code change. You can now imagine having different implementations binding different infrastructure as needed, using dependency injection, factories or whatever you like. But the business code stays the same.

This is something you cannot do with async because it depends on implementation details. However, it doesn’t solve all the problems. TPL tried to solve a problem of callback hell and it did that by encapsulating continuations into an object. People often complain about lambdas in TPL and give async as a solution for that problem (which is not the main benefit but still important factor). Can we get the same encapsulation as above without lambdas?

Typically, a language needs to support this pattern somehow. It’s kind of similar to Continuation Passing Style and the translation can be done by the compiler. Actually, C# supports such a translation for a long time — LINQ with query syntax.

There are plenty of articles how to implement important methods. Here, I’ll just show this little extension:

Now, we can do this:

And this prints 7 as expected. We can implement the same logic for MyTask. Having one interface with all methods implemented as defaults is harder as we don’t have higher kinded types.

The syntax now is very similar to do notation from Haskell or for from Scala. However, this doesn’t address the main point of async which is releasing the thread. We would like to trigger some calculations, let the thread do something else, and then come back to the place where we left. If you think coroutines — you’re on the right track. Actually, these things (coroutines, CPS, fibers, green threads) are pretty similar and can simulate each other (entirely or to some extent). Project Loom in JVM world is exploring this field to implement async in a way which doesn’t result in incompatible method flavours (synchronous and asynchronous).

Being all that said, we can think of a different implementation. You start with a fiber which sits on top of the message loop of the UI thread. It takes a message, creates new fiber and lets it process the message. When the new fiber needs to wait (just like await) it just lets the original fiber to carry on. Original fiber checks the message loop and (at some point) when it realizes the paused fiber can continue, it just schedules it again.

That is not trivial to implement, not to mention that fibers are officially not supported by .NET and are generally considered problematic. C# doesn’t support higher kinded types which limits the options even more.