Updated on 2020-03-19
Learn how to use a powerful programming technique for multitasking in your projects
It doesn't make sense to use a thread-pool for everything. It puts strain on the OS scheduler, usually severely complicates code because of the locking involved, and due to the same can even harm performance. Also, sometimes a thread is just overkill. An alternative to threads when we need multitasking capabilities is to use cooperatively multitasked code. This article aims to teach one pattern for doing such multitasking in your own code.
Coroutines are methods that essentially break up their execution into multiple parts. Each time you call the coroutine, the next part of the task is performed. They essentially can break in the middle of execution and return to where they left off the next time they are called.
C# will generate its own specialized coroutines, referred to as iterators in C#, which divide execution into multiple parts using the yield return statement. As you're probably aware, each time the iterator is moved, the routine picks up where it left off just after the yield return.
Such routines are not magic. They are not special routines. Under the covers of the iterator that C# generates is a state machine in the coroutine itself where each step (each section leading up to a yield return) is a different state. The returned IEnumerator
We'll start by writing our own coroutine from scratch, and then show you how to "cheat" and get C# to generate a "good enough" coroutine for you, despite it being a bit of a kludge/hack.
The following coroutine is a bit contrived, it counts from 1 to 100 and back to 1. Each time you call it, it returns the next number in the series. We can't use a loop inside the routine to accomplish this, so we break it out under a switch case, using a state machine:
static int Coroutine1(Coroutine1Token token)
{
// switch on our state
switch(token.State)
{
case 0: // initially, we increment the value
++token.Value;
// .. until it's 100, then we go to state 1
if(100==token.Value)
token.State = 1;
break;
case 1:
// next, after we're done above we decrement the value
--token.Value;
// .. until it's 1, then we go to state 2
if (1 == token.Value)
token.State = 2;
break;
case 2:
// state 2 is just to tell us we're at the end
// which we signal by returning -1
return -1;
}
// finally, just yield the value we have currently before exiting
return token.Value;
}
The first thing you might notice is Coroutine1Token. There are two main things this class does. It holds any parameters we need to pass to the function, which we don't need in this case, as well as any working state (such as token.Value in this case), and the token.State integer itself which tracks where we are in the coroutine.
Here's how it's called:
var tok = new Coroutine1Token();
int c;
Console.WriteLine("Coroutine1():");
while (-1 != (c = Coroutine1(tok)))
Console.Write(c.ToString() + " ");
Console.WriteLine();
Here, we create a new Coroutine1Token() which Coroutine1() needs in order to function. Then we call Coroutine1() in a loop (like you might do with any coroutine) letting it process each part and it updates the state in Coroutine1Token as necessary. Each iteration of the loop we write the value returned from Coroutine1().
Obviously, you'd do something more useful under each state. However, as you can see, the routine is kind of complicated to build due to the state machine. If we're willing to deal with a kind of ugly interface to it, we can get the C# compiler to do all the heavy lifting using iterators. Enter Coroutine2():
static IEnumerable<int> Coroutine2()
{
// at the first state we count from 1 to 100, yielding each value
for (var i = 1; i <= 100; ++i)
yield return i;
// at the next state we count from 99 to 1, yielding each value
for (var i = 99; 0 < i; --i)
yield return i;
// the final state is implicit, handled by the C# compiler
}
This does the exact same work and returns the same results as Coroutine1(). As you can see, it's quite a bit more intuitive to create. The downside is it's not incredibly intuitive to use. This routine causes the C# compiler to generate something very much like Coroutine1() under the covers. It unrolls the loops and breaks them up if they contain a yield statement, and does similar with if blocks and the like. The end result is a much less complicated way to create a state machine and a coroutine that drives it. The interface however, leaves something to be desired. Here's how we call it:
Console.WriteLine("Coroutine2():");
foreach (var i in Coroutine2())
Console.Write(i.ToString() + " ");
Console.WriteLine();
See how we have to call it using foreach? That's a little weird, especially in cases where you need a while loop or something instead of using foreach. In those cases, you must use the enumerator directly:
Console.WriteLine("Coroutine2() using while:");
using (var e = Coroutine2().GetEnumerator())
{
// each time MoveNext() is called, Coroutine2() is run for a single step
while(e.MoveNext())
{
// e.Current holds the result of Coroutine2()'s step
Console.Write(e.Current.ToString() + " ");
}
}
Console.WriteLine();
See what I mean about not being very intuitive? That's the price you pay for ease of implementation.
So all of this is well and good but what does it have to do with multitasking?
Essentially, every time we call a coroutine, it performs a (hopefully) minor amount of work before yielding time to the caller. Because it yields time in granular chunks, it means it won't lock up your calling thread for the duration. Due to this, you can call a coroutine to do one fragment of work, and then move on to the next thing, even other coroutines.
Fibers are another way to abstract this, and I use that technique in my Lex project. They are like tiny bare bones threads that are scheduled cooperatively instead of preemptively.