[Python-Dev] coroutines and microthreads
Guido van Rossum
guido@python.org
Thu, 16 Nov 2000 22:15:06 -0500
> > This is about as simple as it gets. It supports both generators (like
> > the example here) and coroutines (like Tim's Demo/threads/squasher.py
> > example).
>
> This looks very nice and clean. It looks sufficient for the type
> of thing I'm doing (and planning to do), but is it really a full
> coroutine API? That is, doesn't the fact that you always
> suspend to the guy who just activated you make this a
> generator API? (OTOH, if I want A and B to talk to each other
> as "coroutines", is it sufficient to make them both "generators"
> and then glue them together with another routine that just
> swaps results?)
Yes, it is a full coroutine API -- you don't have to use suspend() at
all. Appended is my version of squasher.py, an almost mechanical
translation from Tim's version (co.tran(cofoobar, arg) => cofoobar(arg)).
That's built out of a bunch of real coroutines with only explicit
transfers!
> > Besides the APIs shown here (coroutine(), suspend(), and EarlyExit) I
> > propose a function current() which returns the current coroutine
> > object. There should also be a way to kill a coroutine (or at least
> > to send an exception). When a coroutine falls through at its end,
> > *some* other coroutine needs to be resumed.
>
> Random thoughts:
> - I've found it handy that Christian's stuff lets you grab a
> coroutine as well as return one (that is, either side can
> instigate it). Not sure if that's necessary.
Sorry, not sure what this means.
> - What do you mean by "kill a coroutine"? You can't interrupt
> one, so isn't it sufficient that when it goes out of scope it gets
> GC'd somehow?
That's one solution. You can't interrupt threads either, and most
people are happy with that (although there's a request for thread
interrupts in PEP 42).
> - It appears from your example that falling off the end
> automatically raises an EarlyExit. I think I see more
> arguments for that than against it :-).
Yes, this follows Tim's lead. What would be an argument against it?
> > Microthreads?
> > -------------
> >
> > I'm much less clear about microthreads (uthreads). Last time, I
> > thought this was a fancy name for coroutines. It isn't! Microthreads
> > are "almost real" threads, with round-robin scheduling. What makes
> > them attractive to some is the fact that they don't use operating
> > system resources: there's no OS-level stack or kernel process table
> > entry associated with them. This also accounts for their biggest
> > weakness: when a microthread is suspended for I/O, *all* microthreads
> > are suspended. In limited contexts, such as a network server, this
> > can be solved by an approach similar to that in Gordon's
> > SelectDispatcher. (It would be a titanic effort to do this for every
> > potentially blocking I/O operation, and it probably wouldn't work well
> > with C extensions.)
>
> Using the raw Win32 API, I think you could come pretty close.
You can do it on Unix too, by trapping open() to always set files in
nonblocking mode. It's just a lot of work because there are so many
calls to trap (cf. your dispatcher, which just traps read, write,
accept, connect). Long ago, there was a user-mode thread package on
Solaris that did this; it's still immortalized in Python/thread_lwp.h
although I doubt that it still works. GNU Pth uses the same approach
for user-mode threads. Hm... This makes me wonder... Maybe instead
of uthreads we could promote Python linked with Pth?
> I've been wondering if it's possible to do something that would
> get Cameron to quit raving about Tcl's event loop ;-).
Cameron is now lobbying for a separation between Tcl and Tk. I guess
that will mean that the event loop will have to be moved back to Tk,
where it started many years ago. :-)
> > I'm not sure what applications require the round-robin scheduling
> > property of uthreads -- certainly Gordon's application would seem to
> > be doing just fine without it (I'm not sure if it is being scheduled
> > round-robin or not).
>
> Each coroutine (or whatever they are) runs until it calls one of
> the SelectDispatcher methods that suspends it. The socket
> methods suspend it until select says the socket is ready;
> yield suspends it till the next time round the select loop
> (which has a timeout). So piggish routines are encouraged to
> yield once in a while.
That's what I figured.
> (While I'm doing things the other way 'round, I believe I could
> use your API without changing SelectDispatcher's API at all.)
I think so too. Maybe that'll be my next exercise. I've already done
8 queens with coroutines (slower than recursion, alas -- should be
faster when coroutines are implemented in C though because you save on
number of function calls). I've also made tho working implementations of
my proposed coro API: one using threads, one using Stackless and the
continuation module. (Found a few quirks in the examples in your
tutorial. Interested in details?)
> > Proper round-robin scheduling for uthreads requires explicit switching
> > code in the Python interpreter. Stackless provides this, at the same
> > place where in regular Python the global interpreter lock is released
> > and re-acquired to give other threads a chance to run. Is this
> > needed?
>
> While I'm not really a uthread user, I think they would give you
> an unqualified "yes". The advantage to explicitly yeilding is
> that (with proper thought) you don't need nasty things like
> locks; the disadvantage (as demonstrated by a particular OS
> with a rabidly fanatical following) is that one jerk can ruin it for
> everybody.
Let me guess -- you don't like the Mac much, do you? :-)
And yes, I noticed that the first thing the uthread API does is define
a bunch of synchronization primitives. :-( I think it wouldn't be
hard to implement the round-robin scheduling, but it's yet more added
complexity...
BTW, how's PEP 219 coming along? :-)
--Guido van Rossum (home page: http://www.python.org/~guido/)
# Coroutine example: general coroutine transfers
# (After Tim Peters)
#
# The program is a variation of a Simula 67 program due to Dahl & Hoare,
# (Dahl/Dijkstra/Hoare, Structured Programming; Academic Press, 1972)
# who in turn credit the original example to Conway.
#
# We have a number of input lines, terminated by a 0 byte. The problem
# is to squash them together into output lines containing 72 characters
# each. A semicolon must be added between input lines. Runs of blanks
# and tabs in input lines must be squashed into single blanks.
# Occurrences of "**" in input lines must be replaced by "^".
#
# Here's a test case:
test = """\
d = sqrt(b**2 - 4*a*c)
twoa = 2*a
L = -b/twoa
R = d/twoa
A1 = L + R
A2 = L - R\0
"""
# The program should print:
# d = sqrt(b^2 - 4*a*c);twoa = 2*a; L = -b/twoa; R = d/twoa; A1 = L + R;
#A2 = L - R
#done
# getline: delivers the next input line to its invoker
# disassembler: grabs input lines from getline, and delivers them one
# character at a time to squasher, also inserting a semicolon into
# the stream between lines
# squasher: grabs characters from disassembler and passes them on to
# assembler, first replacing "**" with "^" and squashing runs of
# whitespace
# assembler: grabs characters from squasher and packs them into lines
# with 72 character each, delivering each such line to putline;
# when it sees a null byte, passes the last line to putline and
# then kills all the coroutines
# putline: grabs lines from assembler, and just prints them
from coro import coroutine, suspend, main, EarlyExit
def getline(text):
for line in string.splitfields(text, '\n'):
codisassembler(line)
def disassembler():
while 1:
card = cogetline()
for i in range(len(card)):
cosquasher(card[i])
cosquasher(';')
def squasher():
while 1:
ch = codisassembler()
if ch == '*':
ch2 = codisassembler()
if ch2 == '*':
ch = '^'
else:
coassembler(ch)
ch = ch2
if ch in ' \t':
while 1:
ch2 = codisassembler()
if ch2 not in ' \t':
break
coassembler(' ')
ch = ch2
coassembler(ch)
def assembler():
line = ''
while 1:
ch = cosquasher()
if ch == '\0':
break
if len(line) == 72:
coputline(line)
line = ''
line = line + ch
line = line + ' ' * (72 - len(line))
coputline(line)
def putline():
try:
while 1:
line = coassembler()
print line
except EarlyExit:
main() # Hack -- resume main coroutine to exit
import string
cogetline = coroutine(getline, test)
coputline = coroutine(putline)
coassembler = coroutine(assembler)
codisassembler = coroutine(disassembler)
cosquasher = coroutine(squasher)
coputline()
print "done"
raw_input("Exit?")
# end of example