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
?
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.
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.
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
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 coroutineeusure_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).Commençons par effacer quelques termes:
async def
ssemble commentaires ci-dessous.
await
sur une coroutineNous 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.
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.
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.