This is a small followup to say that yes, in fact coroutines are awesome for this. I never bothered to try to preserve local variables, it's not worth it. You can just store data that you want to save across a "yield" into a struct. In Oodle whenever you are doing a threaded task, you always have a Work context, so it's easy to just stuff your data in there.
I've always been a big fan of coroutines for game scripting
languages. You can do things like :
Walk to TV
Turn on TV
if exists Couch
Walk to Couch
etc. You just write it like linear imperative code, but in fact some of those things take time and yield out of
So obviously the same thing is great for IO or SPU tasks or whatever. You can write :
Vec3 * array = malloc(...);
io_read( array , ... ); //! yields
Mat3 m = camera.view * ...;
spu_transform(array, m); //! yields
object->m_array = array;
and it just looks like nice linear code, but actually you lose execution there at the ! marks and you will only
proceed after that operation finishes.
To actually implement the coroutines I have to use macros, which is a bit ugly, but not intolerable.
I use the C switch method as previously described; normally I auto-generate the labels for the switch with __COUNTER__
so you can just write :
.. code ..
.. code ..
and the YIELD macro does something like :
N = __COUNTER__;
work->next = N;
(note the braces which mean that variables in one yield chunk are not visible to the next; this means that the failure
of the coroutine to maintain local variables is a compile error and thus not surprising).
The exception is if you want to jump around to different blocks of the coroutine, then you need to manually specify a label and you can jump to that label.
Note that yielding without a dependancy is kind of pointless; these coroutines are not yielding to share the CPU, they are yielding because they need to wait on some async handle to finish. So generally when you yield it's because you have some handle (or handles) to async tasks.
The way a yielding call like "spu_transform(array, m);" in the previous example has to be implemented is by
starting an async spu task, and then setting the handle as a dependency. It would be something like :
#define spu_transform(args) \
Handle h = start_spu_transform(args); \
The coroutine yield will then stop running the work, and the work now has a dependency, so it won't resume until
the dependency is done. eg. it waits for the spu task to complete.
I use coroutines basically every time I have to do some processing on a file. For one thing, to minimize memory use
I need to stream the file through a double-buffer. For another, you often need to open the file before you start
processing, and that needs to be part of the async operation chain as well. So a typical processing coroutine looks
something like :
int WorkFunc( Work * work )
Handle h = ioq_open_file( work->filename );
if open failed -> abort
get file size
h = start read chunk 0
chunkI = 1; // the next chunk is 1
// read of chunkI^1 just finished
// start the next read :
h = ioq_read( chunk[chunkI] );
chunkI ^= 1;
// process the chunk we just received :
process( chunk[chunkI] );
where "YIELD_REPEAT" means resume at the same label so you repeat the current block.
The last block of the coroutine runs over and over, ping-ponging on the double buffer, and yields if the next IO is not done yet when the processing of each block is done.