C# 5’s new “await” keyword is not just for orchestrating and controlling
concurrency on multiple threads; it can also be used to introduce “exotic”
control flow, such as coroutines
(see also here), to a single-threaded
C# program.
I was trying to find a really good sample coroutine problem to solve with
“await”, but all the ones I found given as standard examples (e.g., odd word
problem, same fringe
problem) could be easily (and I
think more suitably) solved with C# 2.0 iterators (because the data only flows
one way between the sub/coroutines needed to solve those problems).
The idea I finally came up with was of merging two ordered sequences of
numbers. In C# 2.0, you could write a Merge method that creates an enumerator
for each input sequence, tracks whether each sequence still has items, and
compares the heads of each sequence to determine which is smaller. This is a
lot of code, and the scaffolding obscures the core of the method, which is the
comparison of the two items:
(I’ve used Console.WriteLine for simplicity; in the real world, you’d probably
want this method to return IEnumerable and yield return the merged
elements.)
In C# 5, the merge could be written with coroutines. Imagine two Merge methods
running in an interleaved fashion (not simultaneously), each processing one
input sequence. If the methods can know what the smallest current item in the
other sequence is, then their implementation is trivial: for each of the
values in their own sequence, compare it to the smallest value from the other
sequence. If it’s smaller, print it; if not, switch to the other method and
let it processs its sequence:
Of course, this code can’t stand alone; it needs a scheduler that will start
both the methods, and also an implementation of SwitchToOther, which will save
this method’s “resume” action, and invoke it later when the second method
switches back to this one. There’s a lot of code here, but much of it is just
the boilerplate that’s required to support the “await” keyword (GetAwaiter,
BeginAwait, EndAwait).
At the end, was it worth it? Yes, the Merge method itself is shorter and
concisely expresses its purpose, but there’s a lot of supporting code
required, which may or may not be reusable to solve different problems. This
method is also not scalable: if we wanted to merge three (or more) sequences,
it’s clearly wrong to have each coroutine pick which of its peers it’s going
to switch to; in that case, we’d definitely want a single consumer that’s
picking the overall next smallest value (perhaps using a min heap for
efficiency if there are lots of sequences being merged).
So while it was an interesting challenge to interleave the execution of two
methods on the same thread using “await”, it isn’t the best solution to this
particular problem. (Maybe there is a similar problem for which “await” really
is the killer solution; if you know of one, I’d love to hear about it.)
That’s not to say that “await” is useless; far from it! There are many other
problems (that we’ve solved more or less well in the Logos code today) that
could be solved far more elegantly and concisely with the “await” keyword; I’m
definitely looking forward to using it.
P.S. As an exercise for the reader, change “Console.WriteLine(value);” (in the
first Merge method) to “await Output(value);” and rewrite the second Merge
method to “yield return” the merged values, so that the merge can happen
lazily.