Off and on for a while now, I have been playing around with a little library I am calling taco - for "task coroutines." After spending some time working on a Unity project and after reading through Charles Blooms many posts on the subject (e.g. Coroutine-centric Architecture), I decided to take a crack at creating a coroutine based task processing system in C++. It has gone through several false starts and rewrites as I tried to come up with a base that I felt was reasonable; I think at this point I am ready to start talking about it.
At the core of taco is a worker coroutine, instances of which are cooperatively scheduled across a set of system threads. Rather than make each task a full blown coroutine with its own execution context, wasteful for many of the types of tasks you might normally expect to execute in a game engine, taco switches between its own internal worker coroutines which are themselves executing a task processing loop. In this way, taco only needs to create and switch to new workers when a task being executed actually requires it (e.g. it waits on some event).
To the API user, fundamentally all they see are the functions Schedule and Switch along with a handful of synchronization objects. A most basic and silly example:
// ...
taco::Schedule([]() -> void {
printf("Hello ");
taco::Switch();
printf("World\n");
});
// ...
The call to Schedule will push the function object in a queue of stealable tasks and eventually the current thread (or a thief), via some worker, will pop that task out of the queue. Once a worker grabs this task it will print out "Hello " and then call Switch. When this happens the task is effectively promoted to a full blown coroutine - internally, the current worker gets scheduled for future execution and then we invoke some other idle worker to continue executing any remaining tasks. If there are no remaining tasks then a worker will check and see if there are any other scheduled workers and invoke them. This means that the worker executing our "Hello World" task eventually gets invoked by some other worker and so we are able to execute our final line printing out "World\n". With the task complete our worker can move on to executing other tasks or yielding to other workers.
That is really the core of it - but we can build on that foundation and introduce some useful constructs. For example, you might have seen the proposal for an async/await facility in some future version of C++. I have coded up a simple future object in taco, using the publicly facing API, that provides similar functionality; it simply wraps the function call in a lambda that forwards the return value and signals an event, waking any other tasks that are waiting on the return value. Some example usage code:
unsigned fibonacci(unsigned n)
{
if (n < 2) return 1;
auto a = taco::Start(fibonacci, n - 1);
auto b = taco::Start(fibonacci, n - 2);
return a + b;
}
In this example both a and b have type taco::future<unsigned> and when we hit the return statement we end up switching away from the current worker until the values in a and b are ready.
Similarly, I have put together some basic code to support generators. Using that we could write a fibonacci generator something like this:
auto fibonacci = taco::StartGenerator([]() -> unsigned {
unsigned a = 0;
unsigned b = 1;
for (;;)
{
taco::YieldValue(b);
unsigned tmp = a + b;
a = b;
b = tmp;
}
});
In this example, fibonacci has type generator<unsigned> with methods read and completed, allowing us to keep reading values from a generator until we detect completion (if it terminates). As the code works now, a generator will yield a value and then will be suspended until that value is read (and it assumes a single reader); once the value is read, the generator is rescheduled so that we can generate the next value.
So that is more or less the high-level state of it right now. Check it out on github and feel free to point and laugh. No, really, I appreciate criticism/feedback. The code is definitely not production ready right now, but I plan to continue to hammer on it. In the short term I am working on some logging and profiling code, and then maybe I will add some OSX support (currently works on Windows and Linux, OSX should be trivial).