2015-05-21 16 views
5

Ho avuto l'ipotesi che se avessi scritto coroutine con ricorsi reciprocamente asincroni, non avrebbero raggiunto l'eccezione massima di profondità di ricorsione, poiché il ciclo degli eventi li chiamava (e si comportava come un trampolino). Questo, tuttavia, non è il caso quando li scrivo in questo modo:Coroutine reciprocamente ricorsive con 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)) 

Quando questo viene eseguito, ottengo RuntimeError: maximum recursion depth exceeded while calling a Python object.

C'è un modo per mantenere la pila di crescere in coroutine ricorsive con asyncio?

+0

ogni volta che "cedi da" ti stai collegando alla prossima chiamata. Hai provato a utilizzare una coda invece? In questo modo puoi distribuire le informazioni e disconnetterti nella successiva coroutine senza collegarle tra loro. – shongololo

risposta

7

Per impedire la crescita della pila, è necessario consentire a ciascuna coroutine di uscire effettivamente dopo aver pianificato la chiamata ricorsiva successiva, il che significa che è necessario evitare l'uso di yield from. Al contrario, si utilizza asyncio.async (o asyncio.ensure_future se si utilizza Python 3.4.4+) per pianificare la prossima coroutine con il ciclo degli eventi e utilizzare Future.add_done_callback per pianificare un callback da eseguire una volta che la chiamata ricorsiva ritorna. Ogni coroutine restituisce quindi un oggetto asyncio.Future, il cui set di risultati all'interno del callback viene eseguito quando la chiamata ricorsiva pianificata viene completata.

E 'probabilmente più facile da capire se effettivamente vedere il codice:

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 

Ora, il codice di esempio in realtà non tornare n per tutto il tragitto lo stack, quindi si potrebbe fare qualcosa di funzionalmente equivalente che è un po 'più semplice:

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

Ma ho il sospetto che si intende davvero tornare n tutta la strada fino.

+0

Ottima risposta - esattamente quello che stavo cercando. Grazie! – caleb

+0

@dano, per curiosità. Nel tuo primo codice, se aeb sono infinite coroutine reciproche che non ritornano mai, questo esploderà la memoria con gli oggetti del futuro. corretta? Cosa salta il secondo? –

0

Ho cambiato il codice in async, await e il tempo misurato. Mi piace molto quanto sia più leggibile.

Future:

import asyncio 

@asyncio.coroutine 
def a(n): 
    fut = asyncio.Future() 
    def set_result(out): 
     fut.set_result(out.result()) 
    if n > 1000: 
     return n 
    else: 
     in_fut = asyncio.async(b(n+1)) 
     in_fut.add_done_callback(set_result) 
    return fut 

@asyncio.coroutine 
def b(n): 
    fut = asyncio.Future() 
    def set_result(out): 
     fut.set_result(out.result()) 
    in_fut = asyncio.async(a(n+1)) 
    in_fut.add_done_callback(set_result) 
    return fut 

import timeit 
print(min(timeit.repeat(""" 
loop = asyncio.get_event_loop() 
loop.run_until_complete(a(0)) 
""", "from __main__ import a, b, asyncio", number=10))) 

Risultato:

% time python stack_ori.py 
0.6602963969999109 
python stack_ori.py 2,06s user 0,01s system 99% cpu 2,071 total 

asincrona, attendono:

import asyncio 

async def a(n): 
    if n > 1000: 
     return n 
    else: 
     ret = await asyncio.ensure_future(b(n + 1)) 
    return ret 

async def b(n): 
    ret = await asyncio.ensure_future(a(n + 1)) 
    return ret 

import timeit 
print(min(timeit.repeat(""" 
loop = asyncio.get_event_loop() 
loop.run_until_complete(a(0)) 
""", "from __main__ import a, b, asyncio", number=10))) 

Risultato:

% time python stack.py 
0.45157229300002655 
python stack.py 1,42s user 0,02s system 99% cpu 1,451 total