Asyncio -- delayed calculation

Zachary Ware zachary.ware+pylist at gmail.com
Mon Nov 28 22:32:05 EST 2016


On Mon, Nov 28, 2016 at 6:48 AM, Steve D'Aprano
<steve+python at pearwood.info> wrote:
> What am I doing wrong?

Give yourself a bit more to debug with, since you're going to want to
do something with the result your expensive calculation anyway:

import asyncio

class Counter:
    def __init__(self, i):
        self.count = 10
        self.i = i

    async def count_down(self):
        print(self, self.i, "starting")
        while self.count > 0:
            # simulate a computation
            await asyncio.sleep(0.5)
            self.count -= 1
        print(self, self.i, "completed")
        return self.i + self.count

async def main():
    pool = [Counter(i) for i in range(5)]
    results = []
    for obj in pool:
        results.append(obj.count_down())
    return results

loop = asyncio.get_event_loop()
print(loop.run_until_complete(main()))


This gives you:

[<coroutine object Counter.count_down at 0x101631fc0>, <coroutine
object Counter.count_down at 0x101659048>, <coroutine object
Counter.count_down at 0x1016590f8>, <coroutine object
Counter.count_down at 0x101659150>, <coroutine object
Counter.count_down at 0x1016591a8>]
asynctest.py:25: RuntimeWarning: coroutine 'Counter.count_down' was
never awaited
  print(loop.run_until_complete(main()))

Ok, so let's fix that by adding an 'await' on line 21 (it's reported
at line 25 because that's when the unawaited coroutines are gc'd):

        results.append(await obj.count_down())

Running that gives:

<__main__.Counter object at 0x10203f978> 0 starting
<__main__.Counter object at 0x10203f978> 0 completed
<__main__.Counter object at 0x1025af710> 1 starting
<__main__.Counter object at 0x1025af710> 1 completed
<__main__.Counter object at 0x1025b60b8> 2 starting
<__main__.Counter object at 0x1025b60b8> 2 completed
<__main__.Counter object at 0x1025b60f0> 3 starting
<__main__.Counter object at 0x1025b60f0> 3 completed
<__main__.Counter object at 0x1025b6128> 4 starting
<__main__.Counter object at 0x1025b6128> 4 completed
[0, 1, 2, 3, 4]

Still not right, only one count_down is run at a time.  But that's
because we're using a synchronous for loop to await our results and
populate the results list.  Naively, I tried an 'async for', but
that's trying to be asynchronous in the wrong place:

Traceback (most recent call last):
  File "asynctest.py", line 25, in <module>
    print(loop.run_until_complete(main()))
  File "/usr/lib/python3.5/asyncio/base_events.py", line 387, in
run_until_complete
    return future.result()
  File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result
    raise self._exception
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "asynctest.py", line 20, in main
    async for obj in pool:
TypeError: 'async for' requires an object with __aiter__ method, got list

So instead, checking the docs suggests using asyncio.gather for
parallel execution of tasks, which takes a variable number of
'coros_or_futures'.  On the first attempt, we saw that we had
accidentally created a list of "coroutine objects", so lets go back
and use that:

--- asynctest.py.orig 2016-11-28 21:03:04.000000000 -0600
+++ asynctest.py 2016-11-28 21:03:35.000000000 -0600
@@ -16,9 +16,10 @@

 async def main():
     pool = [Counter(i) for i in range(5)]
-    results = []
+    coros = []
     for obj in pool:
-        results.append(await obj.count_down())
+        coros.append(obj.count_down())
+    results = asyncio.gather(*coros)
     return results

 loop = asyncio.get_event_loop()

Output:

<__main__.Counter object at 0x1026b6160> 4 starting
<__main__.Counter object at 0x10213f978> 0 starting
<__main__.Counter object at 0x1026b6128> 3 starting
<__main__.Counter object at 0x1026af748> 1 starting
<__main__.Counter object at 0x1026b60f0> 2 starting
<_GatheringFuture pending>

Now we've started everything asynchronously, but it exited way too
fast and never completed anything.  But instead of a list of results,
we got a _GatheringFuture at the end of everything.  So let's await
that:

    results = await asyncio.gather(*coros)

And now we get:

<__main__.Counter object at 0x101eb6160> 4 starting
<__main__.Counter object at 0x10063f978> 0 starting
<__main__.Counter object at 0x101eb6128> 3 starting
<__main__.Counter object at 0x101eaf748> 1 starting
<__main__.Counter object at 0x101eb60f0> 2 starting
<__main__.Counter object at 0x101eb6160> 4 completed
<__main__.Counter object at 0x10063f978> 0 completed
<__main__.Counter object at 0x101eb6128> 3 completed
<__main__.Counter object at 0x101eaf748> 1 completed
<__main__.Counter object at 0x101eb60f0> 2 completed
[0, 1, 2, 3, 4]


And there we have it.  Our final script is:

import asyncio

class Counter:
    def __init__(self, i):
        self.count = 10
        self.i = i

    async def count_down(self):
        print(self, self.i, "starting")
        while self.count > 0:
            # simulate a computation
            await asyncio.sleep(0.5)
            self.count -= 1
        print(self, self.i, "completed")
        return self.i + self.count

async def main():
    pool = [Counter(i) for i in range(5)]
    coros = []
    for obj in pool:
        coros.append(obj.count_down())
    results = await asyncio.gather(*coros)
    return results

loop = asyncio.get_event_loop()
print(loop.run_until_complete(main()))


I hope this is a nice prod to your understanding,
-- 
Zach



More information about the Python-list mailing list