Issue
I had the hypothesis that if I wrote mutually recursive coroutines with asyncio, they would not hit the maximum recursion depth exception, since the event loop was calling them (and act like a trampoline). This, however, is not the case when I write them like this:
import asyncio
@asyncio.coroutine
def a(n):
print("A: {}".format(n))
if n > 1000: return n
else: yield from b(n+1)
@asyncio.coroutine
def b(n):
print("B: {}".format(n))
yield from a(n+1)
loop = asyncio.get_event_loop()
loop.run_until_complete(a(0))
When this runs, I get RuntimeError: maximum recursion depth exceeded while calling a Python object
.
Is there a way to keep the stack from growing in recursive coroutines with asyncio?
Solution
To keep the stack from growing, you have to allow each coroutine to actually exit after it schedules the next recursive call, which means you have to avoid using yield from
. Instead, you use asyncio.async
(or asyncio.ensure_future
if using Python 3.4.4+) to schedule the next coroutine with the event loop, and use Future.add_done_callback
to schedule a callback to run once the recursive call returns. Each coroutine then returns an asyncio.Future
object, which has its result set inside the callback that's run when the recursive call it scheduled completes.
It's probably easiest to understand if you actually see the code:
import asyncio
@asyncio.coroutine
def a(n):
fut = asyncio.Future() # We're going to return this right away to our caller
def set_result(out): # This gets called when the next recursive call completes
fut.set_result(out.result()) # Pull the result from the inner call and return it up the stack.
print("A: {}".format(n))
if n > 1000:
return n
else:
in_fut = asyncio.async(b(n+1)) # This returns an asyncio.Task
in_fut.add_done_callback(set_result) # schedule set_result when the Task is done.
return fut
@asyncio.coroutine
def b(n):
fut = asyncio.Future()
def set_result(out):
fut.set_result(out.result())
print("B: {}".format(n))
in_fut = asyncio.async(a(n+1))
in_fut.add_done_callback(set_result)
return fut
loop = asyncio.get_event_loop()
print("Out is {}".format(loop.run_until_complete(a(0))))
Output:
A: 0
B: 1
A: 2
B: 3
A: 4
B: 5
...
A: 994
B: 995
A: 996
B: 997
A: 998
B: 999
A: 1000
B: 1001
A: 1002
Out is 1002
Now, your example code doesn't actually return n
all the way back up the stack, so you could make something functionally equivalent that's a bit simpler:
import asyncio
@asyncio.coroutine
def a(n):
print("A: {}".format(n))
if n > 1000: loop.stop(); return n
else: asyncio.async(b(n+1))
@asyncio.coroutine
def b(n):
print("B: {}".format(n))
asyncio.async(a(n+1))
loop = asyncio.get_event_loop()
asyncio.async(a(0))
loop.run_forever()
But I suspect you really meant to return n
all the way back up.
Answered By - dano
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.