Différence entre coroutine et future / task dans Python 3.5?

102

Disons que nous avons une fonction factice:

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

Quelle est la différence entre:

import asyncio    

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

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

Et:

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

Remarque : l'exemple renvoie un résultat, mais ce n'est pas l'objet de la question. Lorsque la valeur de retour compte, utilisez à la gather()place de wait().

Quelle que soit la valeur de retour, je recherche des éclaircissements sur ensure_future(). wait(coros)et les wait(futures)deux exécutent les coroutines, alors quand et pourquoi une coroutine devrait-elle être enveloppée ensure_future?

Fondamentalement, quelle est la bonne façon (tm) d'exécuter un tas d'opérations non bloquantes en utilisant Python 3.5 async?

Pour un crédit supplémentaire, que faire si je souhaite regrouper les appels? 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. C'est faisable avec un thread ou un pool de processus, mais existe-t-il un moyen de le faire avecasyncio ?

Mise à jour 2020 (Python 3.7+) : n'utilisez pas ces extraits. Utilisez plutôt:

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)

Pensez également à utiliser Trio , une alternative tierce robuste à asyncio.

tricoter
la source

Réponses:

96

Une coroutine est une fonction génératrice qui peut à la fois produire 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 est comme le Promise objets de Javascript. C'est comme un espace réservé pour une valeur qui se matérialisera dans le futur. Dans le cas mentionné ci-dessus, en attendant les E / S réseau, une fonction peut nous donner un conteneur, une promesse qu'elle remplira le conteneur avec la valeur lorsque l'opération sera terminée. Nous nous accrochons à l'objet futur et lorsqu'il est rempli, nous pouvons appeler une méthode dessus pour récupérer le résultat réel.

Réponse directe: vous n'avez pas besoinensure_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.

Crédits supplémentaires: je choisirais run_in_executoret passerais une Executorinstance pour contrôler le nombre de travailleurs maximum.

Explications et exemples de codes

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

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

La run_until_completeméthode garantirait que la boucle est active jusqu'à ce que l'exécution soit terminée. 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 ensure_futurefonction pour envelopper une coroutine et renvoyer un Taskobjet qui est une sorte de Future. La coroutine est planifiée pour être exécutée dans la boucle d'événements principale lorsque vous appelez ensure_future. L'objet futur / tâche retourné n'a pas encore de valeur mais au fil du temps, lorsque les opérations réseau se terminent, 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 futures au lieu d'utiliser simplement des coroutines.

Regardons un exemple d'utilisation de 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()

Ici, nous avons utilisé la create_taskméthode sur l' loopobjet. ensure_futureplanifierait la tâche dans la boucle d'événements principale. Cette méthode nous permet de programmer une coroutine sur une boucle de notre choix.

Nous voyons également le concept d'ajout d'un rappel à l'aide du add_done_callback méthode sur l'objet de tâche.

A Taskest donelorsque la coroutine renvoie une valeur, lève une exception ou est annulée. Il existe des méthodes pour vérifier ces incidents.

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

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

Masnun
la source
3
J'ai mis à jour ma question pour être un peu plus claire - si je n'ai pas besoin du résultat de la coroutine, dois-je toujours l'utiliser ensure_future()? Et si j'ai besoin du résultat, ne puis-je pas simplement l'utiliser run_until_complete(gather(coros))?
tricoter le
1
ensure_futureprogramme la coroutine à exécuter dans la boucle d'événements. Alors je dirais que oui, c'est obligatoire. Mais bien sûr, vous pouvez également planifier les coroutines en utilisant d'autres fonctions / méthodes. Oui, vous pouvez utiliser gather()- mais rassembler attendra que toutes les réponses soient collectées.
masnun
5
@AbuAshrafMasnun @knite gatheret encapsulez waitles coroutines données en tant que tâches en utilisant ensure_future(voir les sources ici et ici ). Il ne sert donc à rien d'utiliser ensure_futureau préalable, et cela n'a rien à voir avec l'obtention des résultats ou non.
Vincent le
8
@AbuAshrafMasnun @knite Aussi, ensure_futurea un loopargument, donc il n'y a aucune raison d'utiliser loop.create_taskover ensure_future. Et run_in_executorne fonctionnera pas avec les coroutines, un sémaphore devrait être utilisé à la place.
Vincent le
2
@vincent il y a une raison d'utiliser create_taskover ensure_future, voir la documentation . Citationcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi
24

Réponse simple

  • L'appel d'une fonction coroutine ( async def) ne l'exécute PAS. Elle renvoie des objets coroutine, comme la fonction générateur renvoie des objets générateurs.
  • await récupère les valeurs des coroutines, ie "appelle" la coroutine
  • eusure_future/create_task programmez la coroutine pour qu'elle s'exécute sur la boucle d'événements lors de la prochaine itération (sans attendre la fin, comme un thread démon).

Quelques exemples de code

Commençons par clarifier quelques termes:

  • fonction coroutine, celle qui vous async defconvient;
  • objet coroutine, ce que vous obtenez lorsque vous "appelez" une fonction coroutine;
  • task, un objet enroulé autour d'un objet coroutine à exécuter sur la boucle d'événements.

Cas 1, awaitsur une coroutine

Nous créons deux coroutines, awaitune, et utilisons create_taskpour 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 __name__ == "__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ée directement, et task2 a été exécutée dans l'itération suivante.

Cas 2, céder le contrôle à la boucle d'événements

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 asyncio.sleep(1), le contrôle a été renvoyé à la boucle d'événements, et la boucle vérifie les tâches à exécuter, puis exécute la tâche créée par create_task.

Notez que nous appelons d'abord la fonction coroutine, mais pas awaitcelle-ci, donc nous venons de créer une seule coroutine et ne la faisons pas fonctionner. Ensuite, nous appelons à nouveau la fonction coroutine et l'enveloppons dans un create_taskappel, creat_task planifiera en fait l'exécution de la coroutine lors de la prochaine itération. Donc, dans le résultat, create taskest exécuté avant await.

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

Sous la capuche

loop.create_taskappelle réellement asyncio.tasks.Task(), qui appellera loop.call_soon. Et loop.call_soonmettra la tâche en place loop._ready. Lors de chaque itération de la boucle, il vérifie tous les rappels dans loop._ready et l'exécute.

asyncio.wait, asyncio.ensure_futureet en asyncio.gatherfait appeler loop.create_taskdirectement ou indirectement.

Notez également dans la documentation :

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

ospider
la source
1
Merci pour une explication claire! Je dois dire que c'est un design assez terrible. L'API de haut niveau fuit une abstraction de bas niveau, ce qui complique trop l'API.
Boris Burkov
1
Découvrez le projet curio, qui est bien conçu
ospider
Belle explication! Je pense que l'effet de l' await task2appel pourrait être clarifié. Dans les deux exemples, l'appel loop.create_task () est ce qui planifie la tâche2 sur la boucle d'événements. Donc, dans les deux ex, vous pouvez supprimer le await task2et encore task2 sera finalement exécuté. Dans ex2, le comportement sera identique, car await task2je pense que c'est juste la planification de la tâche déjà terminée (qui ne s'exécutera pas une deuxième fois), alors que dans ex1, le comportement sera légèrement différent puisque task2 ne sera pas exécuté tant que main n'est pas terminée. Pour voir la différence, ajoutez print("end of main")à la fin de l'ex1 principal
Andrew
11

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

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

Je mettrai à jour cette réponse lorsque je trouverai une explication définitive sur la façon de regrouper les coroutines / futures.

tricoter
la source
Cela signifie-t-il que pour un objet coroutine c, await céquivaut à await create_task(c)?
Alexey
3

Tiré du BDFL [2013]

Tâches

  • C'est une coroutine enveloppée dans un futur
  • class Task est une sous-classe de la classe Future
  • Donc ça marche aussi avec wait !

  • En quoi diffère-t-il d'une coroutine nue?
  • Il peut progresser sans attendre
    • Tant que vous attendez autre chose, c'est à dire
      • attendre [quelque chose_else]

Dans cet esprit, ensure_futurecela a du sens comme nom pour créer une tâche puisque le résultat du futur sera calculé que vous l' attendiez ou non (tant que vous attendez quelque chose). Cela permet à la boucle d'événements de terminer votre tâche pendant que vous attendez d'autres choses. Notez qu'en Python 3.7 create_taskest le moyen préféré d' assurer un avenir .

Remarque: j'ai changé «rendement de» dans les diapositives de Guido pour «attendre» ici pour la modernité.

crizCraig
la source