Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.python > #74091 > unrolled thread

Emperor's New Coroutines?

Started byMarko Rauhamaa <marko@pacujo.net>
First post2014-07-07 12:42 +0300
Last post2014-07-10 20:56 +0300
Articles 2 — 1 participant

Back to article view | Back to comp.lang.python


Contents

  Emperor's New Coroutines? Marko Rauhamaa <marko@pacujo.net> - 2014-07-07 12:42 +0300
    Re: Emperor's New Coroutines? Marko Rauhamaa <marko@pacujo.net> - 2014-07-10 20:56 +0300

#74091 — Emperor's New Coroutines?

FromMarko Rauhamaa <marko@pacujo.net>
Date2014-07-07 12:42 +0300
SubjectEmperor's New Coroutines?
Message-ID<87lhs5h5dn.fsf@elektro.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.

PEP-380 refers to the classic Dining Philosophers: <URL: http://www.cos
c.canterbury.ac.nz/greg.ewing/python/yield-from/yf_current/Examples/Sch
eduler/philosophers.py> (reproduced below).

Suppose I want to fix the protocol by supplying an "attendant" object
that periodically tells the philosophers to drop whatever they are doing
and start thinking. So I would add:

   schedule(attendant(..., [ plato, socrates, euclid ]))

but how would I modify the philosopher code to support such master
resets?


Marko

========================================================================
from scheduler import *

class Utensil:

  def __init__(self, id):
    self.id = id
    self.available = True
    self.queue = []

  def acquire(self):
    #print "--- acquire", self.id, "avail", self.available
    if not self.available:
      block(self.queue)
      yield
    #print "--- acquired", self.id
    self.available = False

  def release(self):
    #print "--- release", self.id
    self.available = True
    unblock(self.queue)

def philosopher(name, lifetime, think_time, eat_time, left_fork,
right_fork):
  for i in range(lifetime):
    for j in range(think_time):
      print(name, "thinking")
      yield
    print(name, "waiting for fork", left_fork.id)
    yield from left_fork.acquire()
    print(name, "acquired fork", left_fork.id)
    print(name, "waiting for fork", right_fork.id)
    yield from right_fork.acquire()
    print(name, "acquired fork", right_fork.id)
    for j in range(eat_time):
      # They're Python philosophers, so they eat spam rather than
      spaghetti
      print(name, "eating spam")
      yield
    print(name, "releasing forks", left_fork.id, "and", right_fork.id)
    left_fork.release()
    right_fork.release()
  print(name, "leaving the table")

forks = [Utensil(i) for i in range(3)]
schedule(philosopher("Plato", 7, 2, 3, forks[0], forks[1]), "Plato")
schedule(philosopher("Socrates", 8, 3, 1, forks[1], forks[2]),
"Socrates")
schedule(philosopher("Euclid", 5, 1, 4, forks[2], forks[0]), "Euclid")
run()

[toc] | [next] | [standalone]


#74325

FromMarko Rauhamaa <marko@pacujo.net>
Date2014-07-10 20:56 +0300
Message-ID<877g3l85dy.fsf@elektro.pacujo.net>
In reply to#74091
Marko Rauhamaa <marko@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=======================================================

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.python


csiph-web