Emperor's New Coroutines?

Marko Rauhamaa marko at pacujo.net
Thu Jul 10 13:56:09 EDT 2014


Marko Rauhamaa <marko at pacujo.net>:

> The asyncio module comes with coroutine support. Investigating the
> topic on the net reveals that FSM's are for old people and the brave
> new world uses coroutines. Unfortunately, all examples I could find
> seem to be overly simplistic, and I'm left thinking coroutines have
> few practical uses in network programming.
>
> [...]
>
> but how would I modify the philosopher code to support such master
> resets?

Ok. I think I have found the answer to my question:

    asyncio.wait(coroutines, return_when=asyncio.FIRST_COMPLETED)

That facility makes it possible to multiplex between several stimuli.

The code below implements a modified dining philosophers protocol. The
philosophers are accompanied by an assistant who occasionally prods the
philosophers to immediately resume thinking, thus breaking the deadlock.

Whether the coroutine style is easier on the eye than, say, callbacks
and state machines is a matter of personal opinion.


Marko

===clip-clip-clip=======================================================
#!/usr/bin/env python3

import os, sys, asyncio, random, enum, time

T0 = time.time()

def main():
    loop = asyncio.get_event_loop()
    try:
        fork1 = Fork()
        fork2 = Fork()
        fork3 = Fork()
        nag = Nag()
        loop.run_until_complete(asyncio.wait([
                    Philosopher("Plato", fork1, fork2, nag).philosophize(),
                    Philosopher("Nietsche", fork2, fork3, nag).philosophize(),
                    Philosopher("Hintikka", fork3, fork1, nag).philosophize(),
                    assistant(nag) ]))
    finally:
        loop.close()

class Philosopher:
    def __init__(self, name, left, right, nag):
        self.name = name
        self.left = left
        self.right = right
        self.nag = nag

    @asyncio.coroutine
    def philosophize(self):
        yield from self.nag.acquire()
        try:
            while True:
                self.nag_count = self.nag.count
                pending = yield from self.think()
                if pending is None:
                    continue
                pending = yield from self.grab_fork("left", self.left, pending)
                if pending is None:
                    continue
                try:
                    pending = yield from self.wonder_absentmindedly(pending)
                    if pending is None:
                        continue
                    pending = yield from self.grab_fork(
                        "right", self.right, pending)
                    if pending is None:
                        continue
                    try:
                        pending = yield from self.dine(pending)
                        if pending is None:
                            continue
                    finally:
                        self.say("put back right fork")
                        self.right.release()
                    pending = yield from self.wonder_absentmindedly(pending)
                    if pending is None:
                        continue
                finally:
                    self.say("put back left fork")
                    self.left.release()
        finally:
            self.nag.release()

    def say(self, message):
        report("{} {}".format(self.name, message))

    def nagged(self):
        return self.nag.count > self.nag_count

    @asyncio.coroutine
    def think(self):
        self.say("thinking")
        result, pending = yield from multiplex(
            identify(Impulse.TIME, random_delay()),
            identify(Impulse.NAG, self.nag.wait_for(self.nagged)))
        if result is Impulse.NAG:
            self.say("nagged")
            return None
        assert result is Impulse.TIME
        self.say("hungry")
        return pending

    @asyncio.coroutine
    def grab_fork(self, which, fork, pending):
        self.say("grabbing {} fork".format(which))
        result, pending = yield from multiplex(
            identify(Impulse.FORK, fork.acquire()),
            *pending)
        if result is Impulse.NAG:
            self.say("has been nagged")
            return None
        assert result is Impulse.FORK
        self.say("got {} fork".format(which))
        return pending

    @asyncio.coroutine
    def wonder_absentmindedly(self, pending):
        self.say("now, what was I doing?")
        result, pending = yield from multiplex(
            identify(Impulse.TIME, random_delay()),
            *pending)
        if result is Impulse.NAG:
            self.say("nagged")
            return None
        assert result is Impulse.TIME
        self.say("oh, that's right!")
        return pending

    @asyncio.coroutine
    def dine(self, pending):
        self.say("eating")
        result, pending = yield from multiplex(
            identify(Impulse.TIME, random_delay()),
            *pending)
        if result is Impulse.NAG:
            self.say("nagged")
            return None
        assert result is Impulse.TIME
        self.say("that hit the spot!")
        return pending

@asyncio.coroutine
def assistant(nag):
    n = 1
    while True:
        report("assistant sleep {}".format(n))
        yield from asyncio.sleep(n)
        report("assistant nag")
        yield from nag.nag()
        n += 1

def report(info):
    sys.stdout.write("{:9.3f} {}\n".format(time.time() - T0, info))

class Impulse(enum.Enum):
    TIME = 1
    NAG = 2
    FORK = 3

class Fork(asyncio.Lock):
    pass

class Nag(asyncio.Condition):
    count = 0

    @asyncio.coroutine
    def nag(self):
        yield from self.acquire()
        try:
            self.count += 1
            self.notify_all()
        finally:
            self.release()

@asyncio.coroutine
def random_delay():
    yield from asyncio.sleep(random.randint(1, 100) / 10)

@asyncio.coroutine
def identify(result, coroutine):
    yield from coroutine
    return result

@asyncio.coroutine
def multiplex(*coroutines):
    done, pending = yield from asyncio.wait(
        coroutines, return_when=asyncio.FIRST_COMPLETED)
    return done.pop().result(), pending
    
if __name__ == '__main__':
    main()
===clip-clip-clip=======================================================



More information about the Python-list mailing list