Trying to wrap my head around futures and coroutines

Phil Connell pconnell at gmail.com
Wed Jan 15 05:18:32 EST 2014


On Mon, Jan 06, 2014 at 06:56:00PM -0600, Skip Montanaro wrote:
> So, I'm looking for a little guidance. It seems to me that futures,
> coroutines, and/or the new Tulip/asyncio package might be my salvation, but
> I'm having a bit of trouble seeing exactly how that would work. Let me
> outline a simple hypothetical calculation. I'm looking for ways in which
> these new facilities might improve the structure of my code.

This instinct is exactly right -- the point of coroutines and tulip futures is
to liberate you from having to daisy chain callbacks together.


> 
> Let's say I have a dead simple GUI with two buttons labeled, "Do A" and "Do
> B". Each corresponds to executing a particular activity, A or B, which take
> some non-zero amount of time to complete (as perceived by the user) or
> cancel (as perceived by the state of the running system - not safe to run A
> until B is complete/canceled, and vice versa). The user, being the fickle
> sort that he is, might change his mind while A is running, and decide to
> execute B instead. (The roles can also be reversed.) If s/he wants to run
> task A, task B must be canceled or allowed to complete before A can be
> started. Logically, the code looks something like (I fear Gmail is going to
> destroy my indentation):
> 
> def do_A():
> when B is complete, _do_A()
> cancel_B()
> 
> def do_B():
> when A is complete, _do_B()
> cancel_A()
> 
> def _do_A():
> do the real A work here, we are guaranteed B is no longer running
> 
> def _do_B():
> do the real B work here, we are guaranteed A is no longer running
> 
> cancel_A and cancel_B might be no-ops, in which case they need to start up
> the other calculation immediately, if one is pending.

It strikes me that what you have two linear sequences of 'things to do':
    - 'Tasks', started in reaction to some event.
    - Cancellations, if a particular task happens to be running.

So, a reasonable design is to have two long-running coroutines, one that
executes your 'tasks' sequentially, and another that executes cancellations.
These are both fed 'things to do' via a couple of queues populated in event
callbacks.

Something like (apologies for typos/non-working code):


cancel_queue = asyncio.Queue()
run_queue = asyncio.Queue()

running_task = None
running_task_name = ""

def do_A():
    cancel_queue.put_nowait("B")
    run_queue.put_nowait(("A", _do_A()))

def do_B():
    cancel_queue.put_nowait("A")
    run_queue.put_nowait(("B", _do_B()))

def do_C():
    run_queue.put_nowait(("C", _do_C()))

@asyncio.coroutine
def canceller():
    while True:
        name = yield from cancel_queue.get()
        if running_task_name == name:
            running_task.cancel()

@asyncio.coroutine
def runner():
    while True:
        name, coro = yield from run_queue.get()
        running_task_name = name
        running_task = asyncio.async(coro)
        yield from running_task

def main():
    ...
    cancel_task = asyncio.Task(canceller())
    run_task = asyncio.Task(runner())
    ...



Cheers,
Phil




More information about the Python-list mailing list