Asynchronously unload the dishwasher
In a previous blog post, I discussed a very complex and intricate process: how my family unloads our dishwasher using a chain of responsibility. We examined a happy-path scenario in which each person hands a dish to the next. Every step takes the same amount of time, and the process hums along like clockwork. You can almost hear us singing “Whistle While You Work” while we gleefully put away dishes.
Now let’s see what happens when we add a few distractions. (Hint: it involves a List<Func<Func<Task>, Task>>
object.)
Let’s say that, just when my son gives a dish to my wife, the phone rings and she decides to take the call. In the synchronous world, my son would freeze in location and time, dish in his hand, until my wife is finished. But in the asynchronous world, while my wife is on the phone, my son and I can play with Legos, do other chores, or make funny faces at my wife since there is nothing more fun than distracting someone on the phone.
In other words, only the process of unloading the dishwasher is frozen in time. While my wife is on the phone, my son and I can still do other things, and as soon as my wife returns, we can pick up the dishwasher unloading where we left off. So we maximize our occupancy even though one element in the chain is not responding.
Now let’s see how this translates into code.
🔗Unloading asynchronously
A single person (a link in the chain) that is not blocked can be represented as just a method, as in the synchronous version before. But this time, the method needs to be asynchronous. So the return type of the method needs to be changed from void
to Task
, and the method can be marked with the async
keyword.
static async Task Person(Func<Task> next)
{
// Implementation
await next();
}
The Person
method above has a single parameter named next
. In the synchronous version, the parameter was of type Action
. The Action
type is a delegate pointing to any method that returns void
and accepts zero parameters. In the asynchronous version, the return type is of type Task
, so the next
delegate is declared with type Func<Task>
. Func<Task>
is a delegate pointing to any method that returns Task
and accepts zero parameters. As before, passing in the delegate allows us to compose multiple individual elements into a chain of responsibility. But this time, it’s an asynchronous one.
Let’s define an asynchronous function that represents my son:
static async Task Son(Func<Task> next)
{
// son can reach? return; else:
Console.WriteLine("Son can't reach");
await next();
}
When my son’s chore is done, he calls the next
delegate and awaits the outcome of the asynchronous operation, which is the continuation of the asynchronous chain.
Now let’s chain everything together:
public async Task ManualDishwasherUnloading()
{
await Son(() => Wife(() => Husband(() => Done())));
}
The ManualDishwasherUnloading
method contains the chain of the individual links. You can see how we’ve represented each person, or a link in the chain, as a method that matches the signature for a Func<Task>
. In contrast to the synchronous version, the asynchronous version of ManualDishwasherUnloading
needs to return a Task
as well and await
the outcome of the chain. Only by making the full call stack asynchronous can resources working on the dishwasher-unloading process be freed up when awaiting completion of elements in the chain.
We’d probably not enjoy manually writing asynchronous and complex method-chaining, like shown in ManualDishwasherUnloading
, over and over again. Luckily, there’s an even more flexible and maintainable way of doing this.
🔗A better asynchronous chain of responsibility
A simpler and more generic approach is to create a list of functions which are executed one by one until the end of the list is reached:
public async Task MoreFlexibleDishwasherUnloading()
{
var elements = new List<Func<Func<Task>, Task>>
{
Son,
Wife,
Husband,
next => Done()
};
await Invoke(elements);
}
static async Task Invoke(List<Func<Func<Task>, Task>> elements, int currentIndex = 0)
{
if(currentIndex == elements.Count)
return;
var element = elements[currentIndex];
await element(() => Invoke(elements, currentIndex + 1));
}
In the previous post, this was done using a List<Action<Action>>
. That was hard enough to wrap my head around, but it wasn’t until I first wrote this code and declared List<Func<Func<Task>, Task>>
that my head really exploded. Why not just List<Func<Task>>
? Remember the signature of the methods stored in the list is Task LinkInTheChain(Func<Task> next)
. We want the ability to execute them in a generic way. The Invoke
method takes a Func<Func<Task, Task>>
from the list and invokes it by passing itself recursively as the next function parameter. Just like with the synchronous version, the process terminates when the end of the list is reached.
The output of this code would be
Son can't reach
Wife can't reach
Husband put dish away
Dish put away!
As soon as we start introducing more asynchronous links in the chain, the generic approach shows its merits. For example, say you want to ignore dishes that are still wet. We can surround a link in the chain with a wrapper that filters out exceptions:
static async Task IgnoreDishStillWetException(Func<Task> next))
{
try
{
await next();
}
catch(DishStillWetException) { }
}
It’s easy to add IgnoreDishStillWetException
before any step in the chain of responsibility when it is needed.
Now that we have the fundamentals in place, let’s apply them to specific scenarios.
🔗Message handling as a chain of responsibility
Handling a message from a queue can be done as an asynchronous chain of responsibility:
public async Task MessageHandlingChain()
{
var elements = new List<Func<Func<Task>, Task>>
{
RetryMultipleTimesOnFailure,
PickMessageFromTransport,
DeserializeMessage,
DetermineCodeToBeExecuted,
};
await Invoke(elements);
}
The last few code snippets look an awful lot like the ones we had in the previous post about the synchronous chain of responsibility, except we’ve replaced Action<Action>
with Func<Func<Task>>
and all methods returning void
now return Task
instead. This isn’t a coincidence. In the asynchronous world, Task
is the new void
. Just like when my wife gets a call, or IO-intensive work is kicked off, the occurrence of an await
is an opportunity for a thread to go work on other, more pressing tasks. When the asynchronous operation is completed, the continuation of the code is scheduled and executed. In contrast to the synchronous version, the thread driving our chain of responsibility will almost never be blocked and can work on hundreds or thousands of tasks concurrently.
For my son and me, this means we can work on many other chores like preparing dinner or doing the laundry, until my wife gets off the phone. This not only makes my wife happy, but it allows us to get more done in less time, ultimately optimizing for more family/idle time.
A quick note on performance: if we look at only one dishwasher unloading process and compare the synchronous version to the asynchronous one, the former might outperform the latter. In our household, though, we unload the dishwasher multiple times a day, especially when guests come over. By looking at many chores executed concurrently and interleaving them, the asynchronous version will drastically improve our family’s overall chore throughput — or in the messaging world, the message handling throughput. Every worker that can be freed up is worth freeing up since it can participate in getting more work done with less context switching.
🔗Summary
Like in the real world, when unloading the dishwasher, message handling needs to be truly asynchronous to avoid locking resources (such as threads) longer than needed. The asynchronous chain of responsibility achieves that by returning a Task
instead of void
. Message handling almost always requires a concurrency level greater than one. The higher the concurrency, the more important it is to free up threads. All freed-up threads can efficiently participate in handling messages, thus optimizing the system resource usage while minimizing the thread context switches.
For more information, check out my presentation on breaking the chain of responsibility asynchronously or our series on async/await best practices.
About the author: Daniel Marbach is a solution architect at Particular Software and sometimes wakes up in the middle of the night, fearing he forgot a ConfigureAwait(false)
in his code.