Differenza tra coroutine e future / task in Python 3.5?


100

Diciamo di avere una funzione fittizia:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

Qual è la differenza tra:

import asyncio    

coros = []
for i in range(5):
    coros.append(foo(i))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coros))

E:

import asyncio

futures = []
for i in range(5):
    futures.append(asyncio.ensure_future(foo(i)))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))

Nota : l'esempio restituisce un risultato, ma questo non è il fulcro della domanda. Quando il valore restituito è importante, usa gather()invece di wait().

Indipendentemente dal valore di ritorno, cerco chiarezza ensure_future(). wait(coros)ed wait(futures)entrambi eseguono le coroutine, quindi quando e perché dovrebbe essere avvolta una coroutine ensure_future?

Fondamentalmente, qual è il modo giusto (tm) per eseguire un mucchio di operazioni non bloccanti usando Python 3.5 async?

Per un credito extra, cosa succede se voglio raggruppare le chiamate? Ad esempio, devo chiamare some_remote_call(...)1000 volte, ma non voglio schiacciare il server web / database / ecc. Con 1000 connessioni simultanee. Questo è fattibile con un thread o un pool di processi, ma esiste un modo per farlo asyncio?

Aggiornamento 2020 (Python 3.7+) : non utilizzare questi snippet. Usa invece:

import asyncio

async def do_something_async():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(foo(i)))
    await asyncio.gather(*tasks)

def do_something():
    asyncio.run(do_something_async)

Considera anche l'utilizzo di Trio , una robusta alternativa di terze parti ad asyncio.

Risposte:


95

Una coroutine è una funzione generatrice che può produrre valori e accettare valori dall'esterno. Il vantaggio di usare una coroutine è che possiamo mettere in pausa l'esecuzione di una funzione e riprenderla in seguito. In caso di un'operazione di rete, ha senso mettere in pausa l'esecuzione di una funzione mentre aspettiamo la risposta. Possiamo usare il tempo per eseguire altre funzioni.

Un futuro è come gli Promiseoggetti di Javascript. È come un segnaposto per un valore che si materializzerà in futuro. Nel caso sopra menzionato, in attesa di I / O di rete, una funzione può darci un contenitore, promettendoci che riempirà il contenitore con il valore al termine dell'operazione. Ci aggrappiamo all'oggetto futuro e quando è soddisfatto, possiamo chiamare un metodo su di esso per recuperare il risultato effettivo.

Risposta diretta: non serve ensure_futurese non hai bisogno dei risultati. Sono utili se hai bisogno dei risultati o se recuperi le eccezioni.

Crediti extra: sceglierei run_in_executore passerei Executorun'istanza per controllare il numero massimo di lavoratori.

Spiegazioni e codici di esempio

Nel primo esempio, stai usando coroutines. La waitfunzione prende un mucchio di coroutine e le combina insieme. Quindi wait()finisce quando tutte le coroutine sono esaurite (completato / finito restituendo tutti i valori).

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

Il run_until_completemetodo si assicurerebbe che il ciclo sia attivo fino al termine dell'esecuzione. Si noti che in questo caso non si ottengono i risultati dell'esecuzione asincrona.

Nel secondo esempio, stai usando la ensure_futurefunzione per avvolgere una coroutine e restituire un Taskoggetto che è una specie di Future. La coroutine è pianificata per essere eseguita nel ciclo di eventi principale quando si chiama ensure_future. L'oggetto future / attività restituito non ha ancora un valore ma nel tempo, al termine delle operazioni di rete, l'oggetto future manterrà il risultato dell'operazione.

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Quindi in questo esempio, stiamo facendo la stessa cosa tranne che stiamo usando futures invece di usare solo coroutine.

Diamo un'occhiata a un esempio di come utilizzare asyncio / coroutines / futures:

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let's stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

Qui abbiamo utilizzato il create_taskmetodo loopsull'oggetto. ensure_futurepianificherebbe l'attività nel ciclo principale dell'evento. Questo metodo ci consente di programmare una coroutine su un ciclo che scegliamo.

Vediamo anche il concetto di aggiungere un callback utilizzando il add_done_callbackmetodo sull'oggetto task.

A Taskè donequando la coroutine restituisce un valore, solleva un'eccezione o viene annullata. Esistono metodi per controllare questi incidenti.

Ho scritto alcuni post sul blog su questi argomenti che potrebbero aiutare:

Ovviamente puoi trovare maggiori dettagli sul manuale ufficiale: https://docs.python.org/3/library/asyncio.html


3
Ho aggiornato la mia domanda per essere un po 'più chiara: se non ho bisogno del risultato della coroutine, devo ancora usarlo ensure_future()? E se ho bisogno del risultato, non posso semplicemente usarlo run_until_complete(gather(coros))?
Knite

1
ensure_futurepianifica l'esecuzione della coroutine nel ciclo degli eventi. Quindi direi di sì, è obbligatorio. Ma ovviamente puoi programmare le coroutine usando anche altre funzioni / metodi. Sì, puoi usare gather(), ma gather attenderà fino a quando tutte le risposte non saranno raccolte.
masnun

5
@AbuAshrafMasnun @knite gathered waiteffettivamente avvolgere le coroutine fornite come attività utilizzando ensure_future(vedere le fonti qui e qui ). Quindi non ha senso usare in ensure_futureanticipo e non ha nulla a che fare con l'ottenimento o meno dei risultati.
Vincent

8
@AbuAshrafMasnun @knite Inoltre, ensure_futureha un loopargomento, quindi non c'è alcun motivo per utilizzare loop.create_tasksopra ensure_future. E run_in_executornon funzionerà con le coroutine, al suo posto dovrebbe essere usato un semaforo .
Vincent

2
@vincent c'è una ragione per usare create_tasksopra ensure_future, vedere i documenti . Citazionecreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

Risposta semplice

  • Invocare una funzione coroutine ( async def) NON la esegue. Restituisce oggetti coroutine, come la funzione generatore restituisce oggetti generatore.
  • await recupera i valori dalle coroutine, cioè "chiama" la coroutine
  • eusure_future/create_task pianificare la coroutine in modo che venga eseguita sul ciclo di eventi alla successiva iterazione (anche se non aspettando che finiscano, come un thread daemon).

Alcuni esempi di codice

Chiariamo prima alcuni termini:

  • funzione coroutine, quella che tu async defs;
  • oggetto coroutine, cosa ottieni quando "chiami" una funzione coroutine;
  • task, un oggetto avvolto attorno a un oggetto coroutine da eseguire nel ciclo di eventi.

Caso 1, awaitsu una coroutine

Creiamo due coroutine, awaituna e usiamo create_taskper eseguire l'altra.

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

otterrai risultato:

1539486251.7055213 - await
1539486251.7055705 - create_task

Spiegare:

task1 è stato eseguito direttamente e task2 è stato eseguito nella seguente iterazione.

Caso 2, cedere il controllo al loop degli eventi

Se sostituiamo la funzione principale, possiamo vedere un risultato diverso:

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

otterrai risultato:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

Spiegare:

Durante la chiamata asyncio.sleep(1), il controllo è stato restituito al ciclo di eventi e il ciclo verifica le attività da eseguire, quindi esegue l'attività creata da create_task.

Nota che, prima invochiamo la funzione coroutine, ma non awaitquella, quindi abbiamo creato una singola coroutine e non la facciamo funzionare. Quindi, chiamiamo di nuovo la funzione coroutine e la racchiudiamo in una create_taskchiamata, creat_task pianificherà effettivamente l'esecuzione della coroutine alla successiva iterazione. Quindi, nel risultato, create taskviene eseguito prima await.

In realtà, il punto qui è restituire il controllo al ciclo, che potresti usare asyncio.sleep(0)per vedere lo stesso risultato.

Sotto il cappuccio

loop.create_taskeffettivamente chiama asyncio.tasks.Task(), che chiamerà loop.call_soon. E loop.call_sooninserirà il compito loop._ready. Durante ogni iterazione del ciclo, controlla ogni callback in loop._ready e lo esegue.

asyncio.wait, asyncio.ensure_futuree asyncio.gathereffettivamente chiamare loop.create_taskdirettamente o indirettamente.

Nota anche nei documenti :

Le richiamate vengono chiamate nell'ordine in cui sono state registrate. Ogni richiamata verrà chiamata esattamente una volta.


1
Grazie per una chiara spiegazione! Devo dire che è un design piuttosto terribile. L'API di alto livello perde l'astrazione di basso livello, che complica eccessivamente l'API.
Boris Burkov

1
controlla il progetto di curiosità, che è ben progettato
ospider

Bella spiegazione! Penso che l'effetto della await task2chiamata potrebbe essere chiarito. In entrambi gli esempi, la chiamata loop.create_task () è ciò che pianifica task2 sul loop di eventi. Quindi in entrambi gli ex è possibile eliminare await task2e ancora task2 alla fine verrà eseguito. In ex2 il comportamento sarà identico, poiché await task2credo sia solo la pianificazione dell'attività già completata (che non verrà eseguita una seconda volta), mentre in ex1 il comportamento sarà leggermente diverso poiché l'attività2 non verrà eseguita fino al completamento di main. Per vedere la differenza, aggiungi print("end of main")alla fine del main di ex1
Andrew

10

Un commento di Vincent collegato a https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , che mostra che wait()avvolge le coroutine ensure_future()per te!

In altre parole, abbiamo bisogno di un futuro e le coroutine si trasformeranno silenziosamente in esse.

Aggiornerò questa risposta quando troverò una spiegazione definitiva su come raggruppare coroutine / futures.


Significa che per un oggetto coroutine c, await cè equivalente a await create_task(c)?
Alexey

3

Dal BDFL [2013]

Compiti

  • È una coroutine avvolta in un futuro
  • class Task è una sottoclasse della classe Future
  • Quindi funziona anche con wait !

  • Come si differenzia da una coroutine nuda?
  • Può fare progressi senza aspettarlo
    • Finché aspetti qualcos'altro, ad es
      • attendi [something_else]

Con questo in mente, ensure_futureha senso come nome per la creazione di un'attività poiché il risultato del futuro verrà calcolato indipendentemente dal fatto che lo si attenda o meno (purché si attenda qualcosa). Ciò consente al ciclo di eventi di completare l'attività mentre stai aspettando altre cose. Nota che in Python 3.7 create_taskè il modo preferito per garantire un futuro .

Nota: ho cambiato "resa da" nelle diapositive di Guido in "attesa" qui per la modernità.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.