= Solution
Keep a <FIFO queue> of runnable <continuations> in shared <computer memory>. Removing a <continuation> from its head transfers control with `nil`; suspending a runnable computation appends its <continuation> at the rear. A single scheduler group also needs a terminal <continuation> so that finishing the last computation cannot return through an obsolete invocation of `fork`.
The following is `FunMem`, including the shared initialization and helper definitions. `run(main)` starts a scheduler group; nested `fork` calls use that same group.
``
val ready = new(); ready := nil;
val stop = new();
rec append(xs,ys) =
if xs = nil then ys else head(xs) : append(tail(xs),ys);
val enqueue(k) =
ready := append(!ready,k:nil); nil;
val dispatch() =
if !ready = nil then withc(!stop,nil)
else let val k = head(!ready) in
ready := tail(!ready); withc(k,nil);
val run(main) =
callcc(lambda (done)
stop := done; ready := nil;
main(); dispatch());
val fork(f) =
callcc(lambda (parent)
enqueue(parent); f(); dispatch());
val yield() =
callcc(lambda (current)
enqueue(current); dispatch())
``
At `fork`, the parent is queued before `f()` starts. The child runs immediately. Its ordinary return value is ignored by sequencing into `dispatch()`. At `yield`, the current computation joins the rear, then the oldest runnable computation is removed from the front. If there was no other runnable computation, these operations select the just-captured <continuation>, so the caller resumes immediately with `nil`.
The invariant is that each suspended runnable computation has exactly one entry in `ready`, ordered by the time it most recently became suspended and runnable. A running computation is absent from that <FIFO queue>. Neither appending nor removing an entry suspends, so these operations are indivisible relative to this <cooperative multitasking> scheduler. Completing a child removes the child from consideration permanently; it is never enqueued by its completion wrapper.
For the paper's example, call the initial computation $P$ and the forked computation $C$. The <FIFO queue> and writes evolve as follows:
|| Event
|| Running next
|| Ready queue
|| New list head
| Fork starts child
| $C$
| $P$
| —
| Child writes and yields
| $P$
| $C$
| $1$
| Parent writes and yields
| $C$
| $P$
| $3$
| Child writes and finishes
| $P$
| empty
| $2$
| Parent writes and finishes
| scheduler exits
| empty
| $4$
\Image[/past-exam-of-the-computer-science-course-of-the-university-of-oxford/2015/programming-languages-2015-coroutine-schedule.png]
{title=FIFO continuation scheduling of the paper's two coroutines}
Thus the observable list, read after the scheduler group finishes, is
$$
\boxed{[4,2,3,1],\qquad\operatorname{fork}(f)\text{ and }\operatorname{yield}()\text{ resume their callers with nil}.}
$$
The scheduler exit is used only when no runnable <continuations> remain. A main computation can place its desired final observable value in shared <computer memory> and read it after `run` returns. This wrapper also permits already-forked computations to finish if the initial computation finishes before them. Each `run` creates one active scheduler group; calls of `run` are not nested.
Back to article page