4
votes

Comment créer une fonction qui inclut une boucle for non bloquante?

J'essaie de rendre le code ci-dessous asynchrone:

async def count():
    l = []
    for i in range(10000000):
        l.append(i)
    choice = await random.choice(l)
    return choice

Actuellement, il fonctionnera de manière synchrone.

Est-ce parce que la fonction count il manque l'instruction await ?

J'ai essayé de retravailler la fonction pour inclure await:

import asyncio
import random

async def count():
    l = []
    for i in range(10000000):
        l.append(i)
    return random.choice(l)

async def long_task1():
    print('Starting task 1...')
    task_output = await count()
    print('Task 1 output is {}'.format(task_output ))


async def long_task2():
    print('Starting task 2...')
    task_output = await count()
    print('Task 2 output is {}'.format(task_output ))

async def main():
    await asyncio.gather(long_task1(), long_task2())

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

et il démarrera de manière asynchrone (les deux Tâche de démarrage 1 ... et Tâche de départ 2 ... seront imprimés l'un après l'autre), mais j'obtiens une erreur:

TypeError: l'objet int ne peut pas être utilisé dans l'expression 'await'

Je comprends que l'erreur s'est produite parce que le résultat de random.choice (l) n'est pas un attendu (une coroutine), mais je ne sais pas comment résoudre ce problème sans tourner en rond. Dois-je en quelque sorte refactoriser la boucle for en une couroutine seule?


6 commentaires

Je suppose que la boucle dans count est juste un exemple et non votre code réel?


Eh bien, c'est un exercice que j'ai écrit pour moi-même pour comprendre l'asyncio. Donc, du code réel, je suppose: une simulation d'une tâche de longue durée.


@I Funball: oui, mais je ne veux pas du asyncio.sleep dans mon code, je veux la boucle for. Le asyncio.sleep fonctionne car il est en attente. Je cherche un moyen de transformer la boucle for en une boucle attendue.


@barciewicz Je n'ai pas vu cette spécification dans la question. J'essaierai alors de trouver une solution différente. Mais vous voulez la même sortie qui se produirait avec un sommeil?


@barciewicz voir mes modifications. J'ai ajouté un moyen de ne pas changer du tout votre fonction count () ainsi que le moyen de refactoriser la boucle for en coroutine.


@barciewicz wait asyncio.sleep (0) est le moyen de transformer une boucle for (ou tout code de longue durée dans une async def ) en une attente qui se suspend réellement. Si vous ne suspendez jamais, vous ne donnez pas à asyncio une chance de faire son travail. Le code asyncio normal a tendance à être orienté réseau, il n'a donc jamais besoin de le faire.


3 Réponses :


1
votes

Votre code appelle un rassembler qui exécute simultanément long_task1 et long_task2 . Ensuite, vous appelez une attente sur count dans chaque fonction. Mais cela attendra sur ce sous-programme pour terminer. Ainsi, le sous-programme global se terminera toujours avant le début du sous-programme suivant. Vous avez besoin d'une fonction pour suspendre toute la tâche. J'ai créé deux façons de contourner cela. Les deux impliquent la création de nouvelles tâches.

Création d'un nouveau sous-programme:

async def long_task1():
   print('Starting task 1...')
   task_output = await asyncio.shield(count())
   print('Task 1 output is {}'.format(task_output ))


async def long_task2():
   print('Starting task 2...')
   task_output = await asyncio.shield(count())
   print('Task 2 output is {}'.format(task_output ))

Vous pouvez également conserver votre fonction count identique au code d'origine et modifier votre long_task (1/2) comme ceci pour faire de count () une nouvelle tâche:

async def count():
   l = []
   await asyncio.wait_for(loopProcess(l), timeout=1000000.0)
   return random.choice(l)

async def loopProcess(l):
   for i in range(10000000):
      l.append(i)

Vous pouvez également utiliser create_task si vous avez python 3.7.

Source: https://docs.python.org /3/library/asyncio-task.html


0 commentaires

4
votes

Est-ce parce que la fonction de comptage ne dispose pas de l'instruction wait ?

En bref, oui, vous avez correctement identifié le problème. Pour obtenir une exécution parallèle des tâches, vous devez non seulement spécifier async def , mais aussi attendre quelque chose qui suspend l'exécution, renvoyant ainsi le contrôle à la boucle d'événements. En asyncio, c'est typiquement le type d'appel qui bloquerait un programme synchrone, comme un sommeil ou une lecture depuis une socket qui n'est pas encore prête pour la lecture.

Pour forcer une suspension temporaire , vous pouvez ajouter wait asyncio. sleep (0) dans la boucle en count . L'ajout de await devant une fonction ordinaire telle que random.choice , ne fonctionne pas car await nécessite un objet qui implémente l'interface attendue, et dans votre code random.choice renvoie simplement un entier.


6 commentaires

Bonne réponse, dit-il explicitement sans utiliser sleep () cependant. Mais le point global est utile.


@IFunball Je n'ai pas vu ça, mais quand même, sleep (0) est un peu spécial en ce qu'il ne dort pas réellement, il cède juste le contrôle à la boucle d'événements. Voir par exemple cet ancien problème github ou ce problème de doc . Si le code n'abandonne le contrôle de la boucle d'événements nulle part, il faut soit l'exécuter avec run_in_executor soit insérer des points de rendement explicites (faux), il n'y a pas moyen de contourner cela.


Oui je suis d'accord. Je pense que c'est la meilleure solution. Je le dis seulement parce que j'ai utilisé le sommeil dans ma solution originale et ensuite il a dit qu'il ne voulait pas dormir. C'est l'un des commentaires sous le message d'origine.


@ user4815162342: Merci beaucoup! Encore deux questions: 1. Mettre await asyncio.sleep (0) dans la boucle for rend le code beaucoup plus long que le synchrone d'origine, car - je suppose - le contrôle est renvoyé à le contrôleur à chaque itération de la boucle. Est-ce correct? 2. Si await nécessite un objet qui implémente l'interface attendue, et que dans votre code random.choice renvoie simplement un entier , alors pourquoi les exemples de cette page: realpython.com/async-io-python en utilisant i = await randint (0, 10) fonctionne?


@barciewicz Oui, vous devrez restructurer votre code pour produire moins souvent, ou passer à une fonction non asynchrone exécutée à l'aide de run_in_executor . Puisque votre code ne résout pas un problème concret, on ne peut pas dire quelle serait la meilleure approche.


En ce qui concerne la deuxième question, le code lié s'exécute car il définit un async def randint - mais cela n'accomplit rien, car il n'y a pas de wait à l'intérieur. Si vous regardez attentivement, vous remarquerez que ses appelants attendent toujours quelque chose en plus de randint () , comme asyncio.sleep () ou randsleep () . Sans cela, l'auteur serait confronté au même problème que votre code.



1
votes

Pour qu'asyncio fonctionne correctement, vous ne devriez pas avoir de tâche intensive de processeur (boucle serrée grande for) dans la boucle d'événements. Comme il n'y a aucun moyen de sortir de la boucle for. Si vous utilisez un asyncio.sleep explicite dans la boucle, vous entrez et sortez simplement de la coroutine inutilement et ralentissez le tout. Si votre objectif est simplement de voir comment fonctionne asyncio, c'est bien.

Mais dans le monde réel, si vous avez une tâche intensive de processeur, vous avez deux options

  1. Utilisez le multitraitement et déléguez la tâche à un autre processus.
  2. Utilisez une liaison de code native qui libère GIL et utilise des threads.

Comme son nom l'indique, la bibliothèque est pour io asynchrone. async"io"


0 commentaires