Coroutine
Going to learn this in CS343. Actually introduced in SE350.
A coroutine is a routine that can also be suspended at some point and resumed from that point when control returns.
The state of a coroutine consists of:
- an execution location, starting at the beginning of the coroutine and remembered at each suspend.
- an execution state holding the data created by the code the coroutine is executing. ⇒ each coroutine has its own stack, containing its local variables and those of any routines it calls.
- an execution status—active or inactive or terminated—which changes as control resumes and suspends in a coroutine.
Difference between Routine and Coroutine?
A coroutine can suspend its execution and return to the caller without terminating, a coroutine cannot.
- A coroutine is activated at the point of last suspension.
- A routine always starts execution at the beginning and its local variables only persist for a single activation.
Coroutines are designed to be able to pass execution control back and forth between themselves.
Semi-Coroutine
Semi-Coroutine
A semi-coroutine acts asymmetrically, like non-recursive routines, by implicitly reactivating the coroutine that previously activated it.
Full Coroutine
Full Coroutine
A full coroutine acts symmetrically, like recursive routines, by explicitly activating a member of another coroutine, which directly or indirectly reactivates the original coroutine (activation cycle).
Fibonacci Sequence
So the teacher’s motivation for this is first, let’s consider using global variables. But then that isn’t extendible super well to multiple instances.
- You want to encapsulate global variables into structure variables, and then pass these into each coroutine
Ahh i understand, so the state is stored.
resume()transfers to the last
suspend()`.
Example for fibonacci
Coroutine Fibonacci { // : public uBaseCoroutine
int fn; // used for communication
void main() { // distinguished member
int fn1, fn2; // retained between resumes
fn = 0; fn1 = fn;
suspend(); // return to last resume
fn = 1; fn2 = fn1; fn1 = fn;
suspend(); // return to last resume
for ( ;; ) {
fn = fn1 + fn2; fn2 = fn1; fn1 = fn;
suspend(); // return to last resume
}
}
public:
int operator()() { // functor
resume(); // transfer to last suspend
return fn;
}
};
The main program:
int main() {
Fibonacci f1, f2; // multiple instances
for ( int i = 1; i <= 10; i += 1 ) {
cout << f1() << " " << f2() << endl;
}
}
Important
Eliminate computation or flag variables retaining information about execution state.
This is because the variables might change (?) No, because its bad
- In coroutine-based programming, you want to avoid such explicit logic and let the coroutine’s suspension and resumption naturally guide the flow of execution.
The purpose of coroutines is to simplify the flow, making it more linear and removing state-related logic.
- It’s a way to think about the problem
for ( int i = 0; i < 10; i += 1 ) {
if ( i % 2 == 0 ) // even ?
even += digit;
else
odd += digit;
suspend();
}
for (int i = 0; i < 5; i+=1) {
even += digit;
suspend();
odd += digit;
suspend();
}
When possible, simplest coroutine construction is to write a direct (stand-alone) prog
Convert to coroutine by:
- putting processing code into coroutine main,
- converting reads if program is consuming or writes if program is producing to suspend,
- Fibonacci consumes nothing and produces (generates) Fibonacci numbers ⇒ convert writes (cout) to suspends.
- Formatter consumes characters and only indirectly produces output (as side-effect) ⇒ convert reads (cin) to suspends.
- use interface members and communication variables to to transfer data in/out of coroutine
This approach is impossible for advanced coroutine problems.
The tree traversal stuff
template <typename T>
class Btree {
struct Node {
...
};
... // other members
public : _Coroutine Iterator {
Node* cursor;
void walk(Node * node) { // walk tree
if (node == nullptr) return;
if (node->left == nullptr && node->right == nullptr) { // leaf?
cursor = node;
suspend(); // multiple stack frames
} else {
walk(node->left); // recursion
walk(node->right); // recursion
}
}
void main() {
walk(cursor);
cursor = nullptr;
}
public:
Iterator(Btree<T> & btree) : cursor(&btree.root) {}
T* next() {
resume();
return cursor;
}
};
... // other members
};
template <class T>
bool sameFringe(BTree<T>& tree1, BTree<T>& tree2) {
Btree<T>::Iterator iter1(btree1), iter2(btree2); // iterator for each tree
T *t1, *t2;
for (;;) {
t1 = iter1.next();
t2 = iter2.next();
if (t1 == nullptr | | t2 == nullptr) break; // one traversal complete ?
if (*t1 != *t2) return false; // elements not equal ?
}
return t1 == nullptr && t2 == nullptr; // both traversals completed ?
};