ratchetfreak
If you don't have compiler or language support for coroutines you end up needing to implement a state machine for each of them. You'll need to save all variables that are alive at each yield explicitly.
Stack support (yielding in a nested call) will require the entire call stack supporting the yield by being yielding themselves.
Debugging is kinda a pain because it's not always obvious which state each coroutine object is in. This is especially true when the coroutine is created with compiler support because that will tend to hide the resume point.
Thank you for responding. Here are more rambling thoughts:
After reading this reply I found Simon Sathams article
Coroutines in C that uses the stack for keeping track of where to reenter a function. I can see how that pattern resembles what Per wrote for the breakpoint_callback.
| int function(void) {
static int i, state = 0;
switch (state) {
case 0: /* start of function */
for (i = 0; i < 10; i++) {
state = 1; /* so we will come back to "case 1" */
return i;
case 1:; /* resume control straight after the return */
}
}
}
|
I guess two components which consumes data in "lock-step", are never required to use callbacks. For instance a lexer and a parser can be written as the lexer calling into a parser callback; or it could be rewritten using coroutines (we just need one-way communication there so a generator would do, yielding tokens); or it could be rewritten using an iterator-style API where we have state context and a next method, like the next_token() function used in the bitwise lexers. So this applies to all those "data in one form" to "data in another form" problems.
For two components where there's back-and-forth passing of control-flow, we can use two state machines; or threads with message passing between them; or the modules can be rewritten to use coroutines. But, as you mentioned, coroutines comes with a "mental cost". The control flow might be easier to follow for simple cases, but maybe the overhead of keeping track of states will consume the benefits for larger programs? Are there other ways of structuring command loop programs, besides these I mention here?
For 1:N relations between components, say a state change in X needs to cause update of state in N components, we have to rely on callbacks or we have to manually keep track of all state (I guess that's what the immediate mode GUIs do). Coroutines can't help us here.
My confusion is about the interleaving of these concepts:
Granularity of concurrency: Fibers (non-preemptive threads), coroutines, green-threads, kernel-threads, processes, systems
Polymorphism: Tagged unions, class-based inheritance, duck-typing
Syntactic sugar over callbacks: callbacks, promises, async-await, continuations?
Higher level control flow constructs: straight-line code, conditionals+loops, callback, coroutines, iterators, excepions, message passing between threads, message passing between processes, message passing between machines
Data binding: retained mode, immediate mode, reactive. "Who is responsible for keeping track of the program state"?
Does anyone have suggestions on books or articles that clearly describes these concepts?
Are threaded code as used by forth a form of continuation?