Microthreads without Stackless?

Bengt Richter bokr at oz.net
Sun Sep 19 02:33:56 CEST 2004

On 17 Sep 2004 11:09:36 -0700, groups.google at gnosis.cx (David Mertz, Ph.D.) wrote:

>Michael Sparks <michaels at rd.bbc.co.uk> wrote in message news:
>> The real root of the 'problem' "Bryan Olson" is putting forward is the
>> fact that you can only jump between yield points in simple generators,
>> which are inherently single level, rather than nested. (ie the
>> traditional "you can't wrap generators" question)
>Yeah... I know Bryan thinks that's a problem.  Mostly because he
>doesn't actually know what a coroutine is.  But it's true that the
>scheduled coroutines I present in the mentioned article are always
>"flat" (hmm... didn't I read somewhere that: "Flat is better than
>But that's actually just what coroutines ARE.  Bryan seems to want
>some kind of hybrid between actual coroutines and a call stack.  Which
>isn't necessarily bad.  And is probably something various
>languages--like Perl 6--do support.
>It might also be something that your greenlets support.  It looks like
>an interesting project, and I'll have to take a look at it soon, and
>maybe do an article on them.  But I suspect that even there, Bryan
>won't get everything he wants with his additional constraint that he
>not have to "change any code."
>I'm sure Michael gets the distinction, but for other readers, I'll
>point out that my coroutine schedules probably does require a little
>reorganization of more traditional call-chain code.  For example, you
>might want to modify traditional program to allow arbitrary switches
>in flow control using (pseudo-code):
>  def parent():
>      ....do stuff...
>      child()
>      return
>  def child():
>      ...do stuff...
>      yield to uncle
>      return to parent
>  def uncle():
>      ...stuff...
>      yield (back) to child # nephew, I guess
>That won't work with my generator/coroutines using a scheduler.  
>'child()' can only yield one level up, not arbitrarily.  But you can
>"flatten" the exact same flow by making it:
>  def parent():
>      ...do stuff...
>      yield to child
>      return
>  def child():
>      ...do stuff...
>      yield to uncle
>      yield (back) to parent
>  def uncle():
>      ...stuff...
>      yield (back) to child
>This is pseudo-code, of course.  But it's not much different from in
>my article.  The point is just the 'child()'  can't be *called* from
>'parent()', but rather must be *switched* to (via the scheduler, and a
>yielded "next coroutine").  In terms of what code is executed when,
>it's exactly the same thing... but there *are* some nominal changes
>needed in the way you write the code.
Here is a flattening that allows parameter passing and is not pseudocode ;-)
(Not tested beyond what you see, it's just an experimental toy ;-)

It uses the generator functions themselves as keys identifying the coroutines,
which results in simple spelling for an inter-coroutine call, i.e.,

    co.call(other, arg, arg2, key=val, etc=more)

but it doesn't allow multiple instances from the same generator, so one might
want to tweak that as attribute access instead to avoid typing quotes, for
different names, e.g.,

    co.call.name(arg, arg2, key=val, etc=more)

Anyway, the apparent call signature can be changed every time if desired, since
the actual generator is defined with a single length-2 list containing [args, kw]
and is accessible updated on continuing from a yield since mutating content doesn't
change the reference.

You could specify a return call "name" as an argument if you wanted to.
In fact, a pair of return call names, with the second as a continuation on error
makes an interesting pattern.

----< cotest.py >------------------------------------------------------------
# cotest.py
# coroutine toy with arbitrary parameter passing

def caller(gnak): return gnak[0]()

class CO(object):
    def __init__(self):
        self.tasks = {}
        self.ready = []
    def add(self, fun, *args, **kw):
        ak = [args, kw]
        self.tasks[fun] = caller.__get__([fun(ak).next, ak])
    def call(self, fun, *args, **kw):
        #print 'co.call(%r, %r, %r)' %(fun, args, kw)
        t = self.tasks[fun]
        #print 'im_self:', t.im_self
        t.im_self[1][:] = [args, kw]
    def run(self):
        while self.ready:
            try: self.ready.pop(0)()
            except StopIteration: pass

def parent(ak): # ak is [args, kwargs] conventionally
    print ak, 'doing parent stuff before calling child...'
    yield co.call(child,'to child from parent', advice='sois sage')
    print ak, 'doing parent stuff after calling child...'

def child(ak):
    print ak, 'doing child stuff before calling uncle...'
    yield co.call(uncle, message='to uncle from child')
    print ak,  'doing child stuff after calling uncle before calling parent...'
    yield co.call(parent, 'to parent from child', uncle_said=ak[0]) # (back) to parent
    print ak,  'doing child stuff after calling parent...'

def uncle(ak):
    print ak,  'doing uncle stuff before calling child...'
    yield co.call(child, 'to child from uncle: tell parent')
    print ak,  'doing uncle stuff after calling child...'

def test():
    global co
    co = CO()
    co.call(parent, 'first args to parent')
if __name__ == '__main__': test()


[17:35] C:\pywk\corout>cotest.py
[('first args to parent',), {}] doing parent stuff before calling child...
[('to child from parent',), {'advice': 'sois sage'}] doing child stuff before calling uncle...
[(), {'message': 'to uncle from child'}] doing uncle stuff before calling child...
[('to child from uncle: tell parent',), {}] doing child stuff after calling uncle before calling
[('to parent from child',), {'uncle_said': ('to child from uncle: tell parent',)}] doing parent
stuff after calling child...

Bengt Richter

More information about the Python-list mailing list