[E-Lang] Why Not Coroutines? (was: Deadlock-free [was what is good about E?])

Mark S. Miller markm@caplet.com
Wed, 18 Jul 2001 13:36:38 -0700


At 04:45 PM Tuesday 7/17/01, Dan Moniz wrote:
>So, in as much as my motive is to inform, I'd like to have more people 
>consider coroutines. I'm not sure if they make sense for E (I would 
>initially tend to doubt it, since Java already provides threads, but seeing 
>as how this discussion arose out of possible changes to Java thread support, 
>I suppose it's germane), but they do make sense in many other places where 
>threads just seem to be too much bother than their worth.

Apologies for a long answer to a simple question.  The issues raised are 
hard (for me at least) to explain well, but important enough to explain 
anyway ;)

One of E's ancestors, Tclio/Webmart http://www.agorics.com/webmart.html 
http://www.sun.com/960201/cover/webmart.html was based coroutine scheduling. 
On the liveness vs corruption vs expressiveness tradeoff, a pure 
coroutine-based system falls somewhere between a pre-emptive multi-threaded 
system and a pure event-loop system, but closer to the event-loop system. 
Indeed, one can transform a code written for a coroutine system (like 
WebMart) into code for a pure event-loop system (like E).  As is often the 
case with such "equivalence" preserving transforms, the transformation is 
our best means for understanding how these systems do and don't differ.

This particular transformation is already well documented, though in a 
different context.  It is the transformation to continuation-passing style 
(CPS)
http://www.google.com/search?q=%22continuation+passing+style%22&sourceid=opera&num=50

Anywhere a method calls one of the coroutining primitives that may cause it 
to block, transform it instead to put the rest of the method in a when-catch 
block to be scheduled when the coroutine call would have returned.  Of 
course, you must likewise transform all callers of this method, and all 
their callers in turn.

The seeming cost of the above transformation is a non-issue.  Compilers for 
various lambda languages similar to E (eg, Scheme, ML) often choose to 
transform all code to continuation passing style early in compilation, and 
the jury is still out over whether this is a net win or lose on quality of 
the resulting code.

Rather, the issue is expressiveness, but in a surprising way.  Let's start 
by arguing expressiveness in the other direction, and then seeing what's 
wrong with the argument.

<hypothetical-argument>
Since this transformation is possible and correctness preserving, by not 
providing it, E makes the programmer transform their continuations by 
hand.  They could write such a tool to transform their own programs, and so 
get the expressiveness of the continuation notation, while still living 
within the E framework.  Since they can do this, and the result would be 
more expressive, E may as well provide coroutines itself.  Whether E 
provides them by transformation or primitively (but equivalently to the 
result of the transformation) then becomes a mere implementation matter.
</hypothetical-argument>

There are two things wrong with this argument:

A) It identifies "expressiveness" with notational convenience.

By making the programmer manually write in the when/catch (or continuation 
passing) style around possible blockage points, the notation of E programs 
thereby captures a crucial distinction: *When may the state change out from 
under you?*  Zooko writes about Multi-Threaded Hell:

>when looking at the source code of an object, you have to imagine that two or
>more threads could be accessing the object's state at the same time.  Therefore
>you can rely on very little in the way of "known facts about the state" while
>trying to debug a piece of code.

This fits very well with the Hayekian explanation of the concurrency problem 
on http://www.erights.org/elib/concurrency/event-loop.html as one of plan 
interference, and the resulting punch line "Avoid stale stack frames!".  
When coding, I, and I believe most programmers, need a strong convention 
about what boundaries in a program represent places where they need to 
recheck assumption, because things can change since "they" last looked.  (Or 
rather, when the narrative in their head about what the program is doing last 
looked.)  Our naive intuitions from sequential programming are mostly 
inarticulate, but go something like:

1) On entry to a function/method, assume state invariants are intact, but 
within those constraints anything may have changed.  (For "internal" calls, 
you can't even assume state invariants are intact, but you should at least 
be able to be locally aware of this delicate issue.  This should also be 
less of an issue for E for reasons I can explain if there's interest.)

2) From one instruction/expression/statement to the next, assume that the 
immediately accessible world is only affected in the ways I (this code) 
affects it.

3) Between a call and a return, assume that the callee only affects the world 
in way I can understand.  (This is strengthened in a pointer-safe language, 
and especially in a capability language, in that we may have a principled 
basis for reasoning about the what state the callee may and may not affect.)

Advocates of pre-emptive shared-memory multithreading would have us relearn 
#2 and #3.  I say #2 is too valuable and relearning it is too hard, so forget it.

Advocates of coroutines would let us keep #2, but have us relearn #3.  We 
see by the hypothetical transformation, combined with a coroutine-based 
scheduler, that the programmer using such a hypothetical transformer is now 
writing source code that confuses #1 and #3.  Put another way, since the 
transformer silently turns #3 into #1, on return from a call, we would now 
have to be as assumption-free about the current state as we normally are on 
entry to a method.

Traditional pure event-loop languages (Actors...Toontalk) have not had 
call/return, but are faithful to intuitions #1 and #2.

E is faithful to all three intuitions.

"So why doesn't a when-catch body look like the function definition it is?"  
Well, I tried to do so, for the above reasons, but the notation was clunky.  
However, to emphasize that the body is done *later* (ie, when assumptions 
may have changed), I put the "->" in the syntax between the immediate part 
(on the left) and the eventual part (or the right).  I also made the right 
hand side look approximately like a nested function definition it gets 
transformed into.  We'll see if these are good enough reminders.


The other thing wrong with the hypothetical argument?

B) Our hypothetical programmer has no means to transform clients he doesn't 
control.

When a Scheme compiler does a CPS transform, it transforms the whole program 
-- meaning all code in the transitive call graph.  While our hypothetical 
programmer can write and apply a hypothetical transformer to his own code, 
so that he can write in E-with-coroutines.  He cannot transparently cause 
this transformation to be applied to callers of his that he didn't write and 
doesn't control.  Therefore, although he can chose to knowingly endanger 
himself by obscuring #1 and #3 in his notation, he cannot endanger anyone 
else without their knowledge.


        Cheers,
        --MarkM