web-dev-qa-db-fra.com

Quand utiliser et quand ne pas utiliser Python 3.5 `wait`?

Je reçois le flux d'utilisation de asyncio dans Python 3.5 mais je n'ai pas vu de description de ce que je devrais être awaiting et des choses que je devrais pas ou là où ce serait négligeable. Dois-je simplement utiliser mon meilleur jugement en termes de "ceci est une opération IO et devrait donc être awaited"?

24
dalanmiller

Par défaut, tout votre code est synchrone. Vous pouvez en faire des fonctions de définition asynchrones avec async def et "appeler" ces fonctions avec await. Une question plus correcte serait "Quand dois-je écrire du code asynchrone au lieu de synchrone?". La réponse est "Quand vous pouvez en bénéficier". Dans la plupart des cas, comme vous l'avez noté, vous en bénéficierez. Lorsque vous travaillez avec des opérations d'E/S:

# Synchronous way:
download(url1)  # takes 5 sec.
download(url2)  # takes 5 sec.
# Total time: 10 sec.

# Asynchronous way:
await asyncio.gather(
    async_download(url1),  # takes 5 sec. 
    async_download(url2)   # takes 5 sec.
)
# Total time: only 5 sec. (+ little overhead for using asyncio)

Bien sûr, si vous avez créé une fonction qui utilise du code asynchrone, cette fonction doit également être asynchrone (doit être définie comme async def). Mais toute fonction asynchrone peut utiliser librement du code synchrone. Cela n'a aucun sens de convertir du code synchrone en asynchrone sans aucune raison:

# extract_links(url) should be async because it uses async func async_download() inside
async def extract_links(url):  

    # async_download() was created async to get benefit of I/O
    html = await async_download(url)  

    # parse() doesn't work with I/O, there's no sense to make it async
    links = parse(html)  

    return links

Une chose très importante est que toute opération synchrone longue (> 50 ms, par exemple, c'est difficile à dire exactement) gèlera toutes vos opérations asynchrones pendant ce temps:

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # if search_in_very_big_file() takes much time to process,
    # all your running async funcs (somewhere else in code) will be frozen
    # you need to avoid this situation
    links_found = search_in_very_big_file(links)

Vous pouvez éviter qu'il appelle des fonctions synchrones de longue durée dans un processus séparé (et attend le résultat):

executor = ProcessPoolExecutor(2)

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # Now your main process can handle another async functions while separate process running    
    links_found = await loop.run_in_executor(executor, search_in_very_big_file, links)

Un autre exemple: lorsque vous devez utiliser requests dans asyncio. requests.get est simplement une fonction synchrone de longue durée, que vous ne devez pas appeler à l'intérieur du code asynchrone (encore une fois, pour éviter le gel). Mais cela dure longtemps à cause des E/S, pas à cause de longs calculs. Dans ce cas, vous pouvez utiliser ThreadPoolExecutor au lieu de ProcessPoolExecutor pour éviter une surcharge de multitraitement:

executor = ThreadPoolExecutor(2)

async def download(url):
    response = await loop.run_in_executor(executor, requests.get, url)
    return response.text
62
Mikhail Gerasimov

Vous n'avez pas beaucoup de liberté. Si vous devez appeler une fonction, vous devez savoir s'il s'agit d'une fonction habituelle ou d'une coroutine. Vous devez utiliser le mot clé await si et seulement si la fonction que vous appelez est une coroutine.

Si les fonctions async sont impliquées, il devrait y avoir une "boucle d'événement" qui orchestre ces fonctions async. À strictement parler, ce n'est pas nécessaire, vous pouvez exécuter "manuellement" la méthode async en lui envoyant des valeurs, mais vous ne voulez probablement pas le faire. La boucle d'événements garde une trace des coroutines non encore terminées et choisit la suivante pour continuer à fonctionner. Le module asyncio fournit une implémentation de boucle d'événements, mais ce n'est pas la seule implémentation possible.

Considérez ces deux lignes de code:

x = get_x()
do_something_else()

et

x = await aget_x()
do_something_else()

La sémantique est absolument la même: appelez une méthode qui produit de la valeur, lorsque la valeur est prête, affectez-la à la variable x et faites autre chose. Dans les deux cas, la fonction do_something_else Sera appelée uniquement une fois la ligne de code précédente terminée. Cela ne signifie même pas qu'avant ou après ou pendant l'exécution de la méthode asynchrone aget_x Le contrôle sera cédé à la boucle d'événements.

Il y a encore quelques différences:

  • le deuxième extrait ne peut apparaître qu'à l'intérieur d'une autre fonction async
  • La fonction aget_x N'est pas habituelle, mais coroutine (qui est soit déclarée avec le mot clé async, soit décorée comme coroutine)
  • aget_x Est capable de "communiquer" avec la boucle d'événement: c'est-à-dire de lui rapporter certains objets. La boucle d'événement devrait être capable d'interpréter ces objets comme des requêtes pour effectuer certaines opérations (par exemple pour envoyer une requête réseau et attendre une réponse, ou simplement suspendre cette coroutine pendant n secondes). La fonction get_x Habituelle n'est pas en mesure de communiquer avec la boucle d'événements.
1
lesnik