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 and the forked computation . The FIFO queue and writes evolve as follows:
Thus the observable list, read after the scheduler group finishes, isThe 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.Represent a binary semaphore by a two-element list containing the addresses of its busy flag and its waiting FIFO queue. A blocked continuation belongs to the waiting FIFO queue, not the scheduler's runnable FIFO queue. These definitions extend the
FunMem scheduler from the preceding part:val semaphore() =
let val busy = new() in
let val waiting = new() in
busy := false; waiting := nil; busy : waiting : nil;
val acquire(s) =
let val busy = head(s) in
let val waiting = head(tail(s)) in
if !busy then
callcc(lambda (blocked)
waiting := append(!waiting,blocked:nil); dispatch())
else (busy := true; nil);
val release(s) =
let val busy = head(s) in
let val waiting = head(tail(s)) in
if !waiting = nil then (busy := false; nil)
else let val next = head(!waiting) in
waiting := tail(!waiting); enqueue(next); nilA newly created semaphore is free. Acquiring it while free marks it busy and immediately returns
nil. Acquiring it while busy captures the remainder of the caller, appends that continuation to the waiting FIFO queue, and dispatches a runnable computation. No runnable copy of the blocked caller is retained.When a waiting caller exists,
release keeps the busy flag true: the released permit is reserved for that caller. It removes exactly one waiting continuation and appends it to the scheduler's runnable FIFO queue. It does not invoke dispatch, so the releasing computation keeps running. A newcomer cannot steal the reserved permit during the interval before the released caller resumes. That caller's captured continuation resumes after acquire with nil, already possessing the permit.These are FIFO semaphores; the question only requires selecting one waiter, so choosing the oldest is a valid stronger discipline. The updates contain no suspension except at the explicit blocking point, making them atomic relative to this cooperative multitasking scheduler. As allowed in the paper, invalid releases and executions in which every remaining computation is blocked need no error handling; the latter are deadlocks, not a way to make progress.