web-dev-qa-db-fra.com

Différence entre coroutine et future/tâche dans Python 3.5?

Disons que nous avons une fonction fictive:

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

Quelle est la différence entre:

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

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

Et:

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

Note : L'exemple retourne un résultat, mais ce n'est pas l'objet de la question. Lorsque la valeur de retour est importante, utilisez gather() au lieu de wait().

Indépendamment de la valeur de retour, je cherche la clarté sur ensure_future(). wait(coros) et wait(futures) exécutent tous les deux les routines, alors quand et pourquoi une coroutine devrait-elle être enveloppée dans ensure_future?

En gros, quel est le bon moyen (tm) d’exécuter une série d’opérations non bloquantes en utilisant la variable async de Python 3.5?

Pour un crédit supplémentaire, si je veux grouper les appels en lot? Par exemple, je dois appeler some_remote_call(...) 1000 fois, mais je ne veux pas écraser le serveur Web/la base de données/etc. avec 1000 connexions simultanées. Cela est faisable avec un thread ou un pool de processus, mais existe-t-il un moyen de le faire avec asyncio?

80
knite

Un commentaire de Vincent lié à https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , qui montre que wait() encapsule les routines dans ensure_future() pour vous!

En d'autres termes, nous avons besoin d'un avenir et les coroutines seront transformées silencieusement en elles.

Je mettrai à jour cette réponse lorsque je trouverai une explication définitive sur la procédure de traitement par lots de contrats/contrats à terme.

10
knite

Une coroutine est une fonction génératrice qui peut à la fois donner des valeurs et accepter des valeurs de l'extérieur. L'avantage d'utiliser une coroutine est que nous pouvons suspendre l'exécution d'une fonction et la reprendre plus tard. Dans le cas d'une opération réseau, il est judicieux de suspendre l'exécution d'une fonction pendant que nous attendons la réponse. Nous pouvons utiliser le temps pour exécuter d'autres fonctions. 

Un avenir, c'est comme les objets Promise de Javascript. C'est comme un espace réservé pour une valeur qui se matérialisera dans le futur. Dans le cas susmentionné, une fonction peut nous donner un conteneur en attendant les E/S réseau, en lui promettant de le remplir avec la valeur à la fin de l'opération. Nous conservons l'objet futur et, lorsqu'il est rempli, nous pouvons appeler une méthode pour récupérer le résultat réel. 

Réponse directe: Vous n'avez pas besoin de ensure_future si vous n'avez pas besoin des résultats. Ils sont utiles si vous avez besoin des résultats ou si vous récupérez des exceptions. 

Extra Credits: Je choisirais run_in_executor et transmettrais une instance Executor pour contrôler le nombre maximal d'employés. 

Explications et exemples de codes

Dans le premier exemple, vous utilisez des coroutines. La fonction wait prend un tas de coroutines et les combine. Donc, wait() se termine lorsque toutes les routines sont épuisées (terminé/fini, renvoyant toutes les valeurs). 

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

La méthode run_until_complete permet de s'assurer que la boucle est active jusqu'à la fin de l'exécution. Veuillez noter que vous n'obtenez pas les résultats de l'exécution asynchrone dans ce cas. 

Dans le deuxième exemple, vous utilisez la fonction ensure_future pour envelopper une coroutine et renvoyer un objet Task qui est une sorte de Future. La coroutine est programmée pour être exécutée dans la boucle d'événements principale lorsque vous appelez ensure_future. L'objet future/tâche renvoyé n'a pas encore de valeur, mais avec le temps, une fois les opérations réseau terminées, l'objet futur contiendra le résultat de l'opération. 

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

Donc, dans cet exemple, nous faisons la même chose, sauf que nous utilisons des contrats à terme au lieu de simplement utiliser des coroutines. 

Regardons un exemple d'utilisation d'Asyncio/Coroutines/Futurs:

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

Ici, nous avons utilisé la méthode create_task sur l'objet loop. ensure_future planifierait la tâche dans la boucle d'événements principale. Cette méthode nous permet de planifier une coroutine sur une boucle de notre choix. 

Nous voyons également le concept d'ajouter un rappel en utilisant la méthode add_done_callback sur l'objet de tâche. 

Task est done lorsque la coroutine renvoie une valeur, déclenche une exception ou est annulée. Il existe des méthodes pour vérifier ces incidents. 

J'ai écrit quelques articles de blog sur ces sujets qui pourraient aider:

Vous pouvez bien sûr trouver plus de détails sur le manuel officiel: https://docs.python.org/3/library/asyncio.html

76
masnun

La réponse simple est

  • Appeler une fonction de croutine (async def) ne l'exécute pas. elle renvoie uniquement les objets coroutine, comme la fonction de générateur renvoie les objets générateurs.
  • await récupère les valeurs des coroutines, c’est-à-dire appelle la coroutine
  • eusure_future/create_task planifie l'exécution de la coroutine sur la boucle d'événements lors de la prochaine itération (bien que ne les attende pas, comme un thread de démon).

Quelques exemples de code

Commençons par effacer quelques termes:

  • fonction coroutine, celle que vous async defs
  • coroutine, ce que vous avez quand vous appelez une fonction corotine

semble commentaires ci-dessous.

Cas 1, await sur une coroutine

Nous créons deux coroutines, await one, et utilisons create_task pour exécuter l’autre.

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 __== "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

vous obtiendrez le résultat:

1539486251.7055213 - await
1539486251.7055705 - create_task

Explique:

task1 a été exécuté directement et task2 a été exécuté lors de l'itération suivante.

Cas 2, cédant le contrôle à la boucle d'événement

Si nous remplaçons la fonction principale, nous pouvons voir un résultat différent:

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

vous obtiendrez le résultat:

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

Explique:

Lors de l'appel de asyncio.sleep(1), le contrôle a été restitué à la boucle d'événements et la boucle vérifie l'exécution des tâches, puis exécute la tâche créée par create_task.

Notez que nous appelons d’abord la fonction corotine, mais pas await, nous avons donc créé une seule corotine et ne l’avons pas exécutée. Ensuite, nous appelons à nouveau la fonction corotine et nous l'enveloppons dans un appel create_task. Creat_task planifiera l'activation de la coroutine à la prochaine itération. Donc, dans le résultat, create task est exécuté avant await.

En fait, le but ici est de redonner le contrôle à la boucle, vous pouvez utiliser asyncio.sleep(0) pour voir le même résultat.

Sous la capuche

loop.create_task appelle réellement asyncio.tasks.Task(), qui appellera loop.call_soon. Et loop.call_soon mettra la tâche dans loop._ready. À chaque itération de la boucle, il vérifie chaque rappel de loop._ready et l'exécute.

asyncio.wait, asyncio.eusure_future et asyncio.gather appelez directement loop.create_task directement ou indirectement.

Notez également dans docs :

Les rappels sont appelés dans l'ordre dans lequel ils sont enregistrés. Chaque rappel sera appelé exactement une fois.

7
ospider