Python 与asyncio相互递归的协程

Python 与asyncio相互递归的协程,python,recursion,python-asyncio,Python,Recursion,Python Asyncio,我的假设是,如果我使用asyncio编写相互递归的协程,它们将不会遇到最大递归深度异常,因为事件循环正在调用它们(并且像蹦床一样)。然而,当我这样写的时候,情况并非如此: 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):

我的假设是,如果我使用asyncio编写相互递归的协程,它们将不会遇到最大递归深度异常,因为事件循环正在调用它们(并且像蹦床一样)。然而,当我这样写的时候,情况并非如此:

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))
当这个运行时,我得到
RuntimeError:调用Python对象时超过了最大递归深度


有没有一种方法可以防止堆栈在使用asyncio的递归协程中增长?

要防止堆栈增长,您必须允许每个协程在调度下一个递归调用后实际退出,这意味着您必须避免使用
产生的收益。相反,您可以使用(或
asyncio.sure\u future
if using Python 3.4.4+)来安排与事件循环的下一个协同程序,并使用来安排在递归调用返回时运行回调。然后,每个协同程序返回一个
asyncio.Future
对象,该对象在其计划的递归调用完成时运行的回调中具有其结果集

如果您实际看到代码,可能最容易理解:

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
现在,您的示例代码实际上并不会一直返回堆栈,因此您可以制作一些功能上等效的东西,这稍微简单一些:

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()

但我怀疑你真的想一路返回
n

我将代码更改为
async
等待
并测量时间。我真的很喜欢它的可读性

未来:

结果:

% time python stack_ori.py 
0.6602963969999109
python stack_ori.py  2,06s user 0,01s system 99% cpu 2,071 total
% time  python stack.py
0.45157229300002655
python stack.py  1,42s user 0,02s system 99% cpu 1,451 total
异步,等待:

结果:

% time python stack_ori.py 
0.6602963969999109
python stack_ori.py  2,06s user 0,01s system 99% cpu 2,071 total
% time  python stack.py
0.45157229300002655
python stack.py  1,42s user 0,02s system 99% cpu 1,451 total

在Python3.7中,可以通过使用
asyncio.create_task()
实现“蹦床”效果,而不是直接等待协同程序

import asyncio

async def a(n):
    print(f"A: {n}")
    if n > 1000: return n
    return await asyncio.create_task(b(n+1))

async def b(n):
    print(f"B: {n}")
    return await asyncio.create_task(a(n+1))

assert asyncio.run(a(0)) == 1002
但是,这样做的缺点是事件循环仍然需要跟踪所有中间任务,因为每个任务都在等待其后续任务。我们可以使用
Future
对象来避免此问题

import asyncio

async def _a(n, f):
    print(f"A: {n}")
    if n > 1000:
        f.set_result(n)
        return
    asyncio.create_task(_b(n+1, f))

async def _b(n, f):
    print(f"B: {n}}")
    asyncio.create_task(_a(n+1, f))

async def a(n):
    f = asyncio.get_running_loop().create_future()
    asyncio.create_task(_a(0, f))
    return await f

assert asyncio.run(a(0)) == 1002

每次你“屈服于”时,你都在接下一个电话。你试过使用队列吗?这样你就可以把信息传递出去,不必互相链接就可以进入下一个协同程序。很好的答案——正是我想要的。谢谢@达诺,出于好奇。在您的第一个代码中,如果a和b是永不返回的无限相互协程,这将爆炸未来对象的内存。对的第二个呢?