Fake threads (was [Python-Dev] ActiveState & fork & Perl)

Tim Peters tim_one@email.msn.com
Wed, 7 Jul 1999 00:18:13 -0400


[Mark Hammond]
> ...
> Thanks for the considerable time it must be taking to enlightening us!

You're welcome, but the holiday weekend is up and so is my time.  Thank (all
of) *you* for the considerable time it must take to endure all this <wink>!

Let's hit the highlights (or lowlights, depending on your view):

> ...
> Im a little confused by how these [continuations] work in practice.

Very delicately <no wink>.  Eariler I posted a continuation-based
implementation of generators in Scheme, and Sam did the same for a
hypothetical Python with a call/cc equivalent.  Those are typical enough.
Coroutines are actually easier to implement (using continuations), thanks to
their symmetry.

Again, though, you never want to muck with continuations directly!  They're
too wild.  You get an expert to use them in the bowels of an implementation
of something else.

> ...
> It is clear to me how you can capture the "state" of a running program.
> Indeed, this is exactly what it seems generators and coroutines do.

Except that generators need only worry about their own frame.  Another way
to view it is to think of the current computation being run by a (real
<wink>) thread -- then capturing a continuation is very much like making a
frozen clone of that thread, stuffing it away somewhere for later thawing.

> With continuations, how is the state captured or created?

There are, of course, many ways to implement these things.  Christian is
building them on top of the explicit frame objects Python already creates,
and that's a fine way for Python.  Guido views it as cloning the call stack,
and that's accurate too.

>> Anyway, in implementation terms a continuation "is like" what
>> a coroutine would be if you could capture its resumption state at
>> any point (even without the coroutine's knowledge!) and assign that
>> state to a vrbl.

> This makes sense, although it implies a "running state" is necessary for
> this to work.

In implementations (like Chris's) that do it all dynamically at runtime, you
bet:  you not only need a "running state", you can only capture a
continuation at the exact point (the specific bytecode) you run the code to
capture it.  In fact, there *is* "a continuation" at *every* dynamic
instance of every bytecode, and the question is then simply which of those
you want to save <wink>.

> In the case of transfering control to somewhere you have never been
> before (eg, a goto or a new function call) how does this work?

Good eye:  it doesn't in this scheme.  The "goto" business is a theoretical
transformation, in a framework where *every* operation is modeled as a
function application, and an entire program's life is modeled as a single
function call.  Some things are very easy to do in theory <wink>.

> Your example:
>> def test():
>>     i = 0
>>     global continuation
>>     continuation = magic to resume at the start of the next line
>>     i = i + 1
>>     return i

> My main problem is that this looks closer to your description of a kind-of
> one-sided coroutine - ie, instead of only being capable of transfering
> control, you can assign the state.  I can understand that fine.

Good!

> But in the example, the function _is_ aware its state is being
> captured - indeed, it is explicitely capturing it.

In real life, "magic to resume at the start of the next line" may be spelled
concretely as e.g.

    xyz(7)

or even

    a.b

That is, anywhere in "test" any sort of (explicit or implicit) call is made
*may* be part of a saved continuation, because the callee can capture one --
with or without test's knowledge.

> My only other slight conceptual problem was how you implement functions,
> as I dont understand how the concept of return values fits in at all.

Ya, I didn't mention that.  In Scheme, the act of capturing a continuation
returns a value.  Like so:

(define c #f)  ; senseless, but Scheme requires definition before reference

(define (test)
  (print
   (+ 1
      (call/cc
       (lambda (k)
         (set! c k)
         42))))
  (newline))

The function called by call/cc there does two things:

1) Stores call/cc's continuation into the global "c".
2) Returns the int 42.

> (test)
43
>

Is that clear?  The call/cc expression returns 42.  Then (+ 1 42) is 43;
then (print 43) prints the string "43"; then (newline) displays a newline;
then (test) returns to *its* caller, which is the Scheme shell's
read/eval/print loop.

Now that whole sequence of operations-- what happens to the 42 and
beyond --*is* "call/cc's continuation", which we stored into the global c.
A continuation is itself "a function", that returns its argument to the
context where the continuation was captured.

So now e.g.

> (c 12)
13
>

c's argument (12) is used in place of the original call/cc expression; then
(+ 1 12) is 13; then (print 13) prints the string "13"; then (newline)
displays a newline; then (test) returns to *its* caller, which is *not* (c
12), but just as originally is still the Scheme shell's read/eval/print
loop.

That last point is subtle but vital, and maybe this may make it clearer:

> (begin
    (c 12)
    (display "Tim lied!"))
13
>

The continuation of (c 12) includes printing "Tim lied!", but invoking a
continuation *abandons* the current continuation in favor of the invoked
one.  Printing "Tim lied!" wasn't part of c's future, so that nasty slur
about Tim never gets executed.  But:

> (define liar #f)
> (begin
    (call/cc (lambda (k)
               (set! liar k)
               (c 12)))
    (display "Tim lied!")
    (newline))
13
> (liar 666)
Tim lied!
>

This is why I stick to trivial examples <wink>.

> And one final question:  In the context of your tutorial, what do Chris'
> latest patches arm us with?  Given my new-found expertise in this matter
> <wink> I would guess that the guts is there to have at least co-routines,
> as capturing the state of a running Python program, and restarting it
> later is possible.  Im still unclear about continuations WRT "without the
> co-routines knowledge", so really unsure what is needed here...

Christian is taking his work very seriously here, and as a result is
flailing a bit trying to see whether it's possible to do "the 100% right
thing".  I think he's a lot closer than he thinks he is <0.7 wink>, but in
any case he's at worst very close to having full-blown continuations
working.  Coroutines already work.

> The truly final question:-) Assuming Chris' patches were 100% bug free and
> reliable (Im sure they are very close :-) what would the next steps be to
> take advantage of it in a "clean" way?  ie, assuming Guido blesses them,
> what exactly could I do in Python?

Nothing.  What would you like to do?  Sam & I tossed out a number of
intriguing possibilities, but all of those build *on* what Christian is
doing.  You won't get anything useful out of the box unless somebody does
the work to implement it.

I personally have wanted generators in Python since '91, because they're
extremely useful in the useless things that I do <wink>.  There's a
thread-based generator interface (Generator.py) in the source distribution
that I occasionally use, but that's so slow I usually recode in Icon (no,
I'm not a Scheme fan -- I *admire* it, but I rarely use it).  I expect
rebuilding that on Christian's work will yield a factor of 10-100 speedup
for me (beyond losing the thread/mutex overhead, as Chris just pointed out
on c.l.py resumption should be much faster than a Python call, since the
frame is already set up and raring to go).

Would be nice if the language grew some syntax to make generators pleasant
as well as fast, but the (lack of) speed is what's really killing it for me
now.

BTW, I've never tried to "sell" coroutines -- let alone continuations.  Just
generators.  I expect Sam will do a masterful job of selling those.

send-today-don't-delay-couldn't-give-or-receive-a-finer-gift-ly y'rs  - tim