Async Wandering Part 11 — Wrapping fibers in context

This is the eleventh 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?

We continue exploring async code.

Last time we saw how to use fibers to wait tasks. This effectively allows us to have async code without any function coloring.

We already know function coloring has big drawbacks and I also provided an example how to use monads to address that. However, monads were actually reverse-coloring everything, instead of having functions of two colors (one for sync and one for async) we had all functions of the same color with some monad type returned.

Today we are going to merge that approach. We’ll have reverse-colored functions with fibers. Let’s begin.

I start with the same glue code as the last time:

Next, I introduce monads:

Super simple interface, not adhering to all monad laws. This is just for showing the idea, not a bullet proof implementation.

We go with this base class:

We can see that Value is a glue to just extract the value from the monad. It’s an unwrap operation.

Next, we have our identity monad:

We have value, simple map operation waiting for the value to appear, and a callback to fill the monad. Notice how this is effectively a promise.

Now, async monad:

The only difference here is that we do the cooperative scheduling instead of just pausing the thread.

Now, we’d like to decide how to use these monads. For that we need to have builders which we’ll be able to replace in runtime (similar to synchronization context):

No magic here. And the env:

If you think about task builders in C# (which let you return any task type) then you’re right.

Okay, let’s go with some operation now. Again, I’ll have 2 jobs:

We create environment with builders and then continue:

We glue some lambdas together thanks to mapping. Notice how we return delay from the first lambda and then call Map on it which makes the waiting. Also, notice how I pass environment explicitly. In some other language we could pass it via implicits, or we could utilize compiler to do the reverse-coloring for us (to avoid parameters and lambdas!).

Moving on:

Instead of blocking the thread with sleep, we create a timer which calls the callback and resolves the monad.

Here we just return the data.

And another side job:


Okay, works like a charm. However, let’s now do some magic. Instead of using async builder for async methods lets do AsyncBuilder = new IdBuilder(). Output:

Notice that Side job waited until the first job finished and wasn’t run in the middle. The thread was sleeping since we blocked it by using synchronous monad. One line of code and we disabled asynchronous code.

Is this approach better? In theory this has the best of two worlds, you can wait synchronously for async code, not block the thread, and change the logic any way you like. Allocation is much higher, but that’s obvious with this approach. Also, there is a lot of plumbing code so we’d probably need a clever compiler doing the magic behind the scenes but that could bring us back to the C# solution. Apart from different coloring, I pass environment explicitly but once I stop doing that (and it’s added by the compiler) then it isn’t much different from thread local variables for synchronization context.

However, the difference now is that nothing can escape the monad (well, at least when we have compiler checking that) so we can control how things behave deep down. If we better encapsulate the environment then it won’t be possible to change the async machinery by replacing some global state.

Ultimately these solutions may not necessarily differ that much.