295 votes

Comment asyncio fonctionne-t-il réellement ?

Cette question est motivée par mon autre question: Comment vous attendent dans le cdef?

Il y a des tonnes d'articles et de billets de blog sur le web à propos de asyncio, mais ils sont tous très superficielle. Je ne pouvais pas trouver toutes les informations à propos de la façon dont asyncio est réellement mis en œuvre, et de ce fait I/O asynchrone. J'étais en train de lire le code source, mais il y a des milliers de lignes de pas le grade le plus élevé de code C, beaucoup de qui traite avec l'auxiliaire objets, mais plus important encore, il est difficile de se connecter entre la syntaxe de Python et de ce que le code C, il se serait traduit par des.

Asycnio propre documentation est encore moins utile. Il n'y a pas d'informations sur la façon dont il fonctionne, seulement quelques lignes directrices sur la façon d'utiliser, et qui sont aussi parfois trompeuses très mal écrit.

Je suis habitué à Aller de mise en œuvre de coroutines, et était un peu en espérant que Python a fait la même chose. Si c'était le cas, le code que j'ai fourni dans le post ci-dessus ont travaillé. Puisqu'il n'a pas, je vais maintenant essayer de comprendre pourquoi. Ma meilleure supposition jusqu'à présent est la suivante, s'il vous plaît corrigez-moi où je me trompe:

  1. Définitions de procédure de la forme async def foo(): ... sont en fait interprétés comme les méthodes d'une classe héritant coroutine.
  2. Peut-être, async def est en fait divisée en plusieurs méthodes en await états, où l'objet, sur lequel ces méthodes sont appelées est en mesure de suivre les progrès accomplis grâce à l'exécution de la mesure.
  3. Si ce qui précède est vrai, alors, essentiellement, de l'exécution d'une coroutine se résume à l'appel de méthodes de coroutine objet par certains gestionnaire global (en boucle?).
  4. Le gestionnaire global est d'une certaine manière (comment?) conscient de quand I/O sont les opérations réalisées par Python (seulement?) code et est en mesure de choisir l'un de l'attente de la coroutine méthodes d'exécuter la suite de l'actuel de l'exécution de la méthode cédé le contrôle (frapper sur l' await déclaration).

En d'autres termes, voici ma tentative de "desugaring" de certains asyncio de la syntaxe en quelque chose de plus compréhensible:

async def coro(name):
    print('before', name)
    await asyncio.sleep()
    print('after', name)

asyncio.gather(coro('first'), coro('second'))

# translated from async def coro(name)
class Coro(coroutine):
    def before(self, name):
        print('before', name)

    def after(self, name):
        print('after', name)

    def __init__(self, name):
        self.name = name
        self.parts = self.before, self.after
        self.pos = 0

    def __call__():
        self.parts[self.pos](self.name)
        self.pos += 1

    def done(self):
        return self.pos == len(self.parts)


# translated from asyncio.gather()
class AsyncIOManager:

    def gather(*coros):
        while not every(c.done() for c in coros):
            coro = random.choice(coros)
            coro()

Si ma supposition se confirme: alors j'ai un problème. Comment I/O se produisent réellement dans ce scénario? Dans un thread séparé? Est l'ensemble de l'interprète suspendu et I/O qui se passe à l'extérieur de l'interprète? Qu'entend-on exactement par I/O? Si mon python procédure appelée C open() procédure, et il à son tour a envoyé interrompre à noyau, de l'abandonner à elle, comment ne interpréteur Python savoir à ce sujet et est en mesure de poursuivre l'exécution de certains autres code, alors que le code du noyau de l'I/O et jusqu'à ce qu'il se réveille le Python de la procédure qui a envoyé l'interrompre à l'origine? Comment peut-interpréteur Python en principe, être conscient de ce qui se passe?

514voto

Bharel Points 5784

Comment asyncio travail?

Avant de répondre à cette question, nous avons besoin de comprendre un peu la base de termes, ignorer ces si vous connaissez déjà l'un d'eux.

Générateurs

Les générateurs sont des objets qui nous permettent de suspendre l'exécution d'une fonction python. L'utilisateur organisée générateurs de mettre en œuvre en utilisant le mot-clé yield. Par la création d'une fonction normale contenant de l' yield mot-clé, nous nous tournons fonction dans un générateur:

>>> def test():
...     yield 1
...     yield 2
...
>>> gen = test()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Comme vous pouvez le voir, appelant next() sur le générateur entraîne l'interprète de test de charge du châssis, et de retour à l' yielded valeur. Appelant next() encore, provoquer le cadre de charger à nouveau dans l'interprète de la pile, et continuer sur yielding une autre valeur.

Par la troisième fois next() est appelé, notre générateur était fini, et StopIteration a été levée.

Communiquer avec un générateur

Moins connu des générateurs, est le fait que vous pouvez communiquer avec eux à l'aide de deux méthodes: send() et throw().

>>> def test():
...     val = yield 1
...     print(val)
...     yield 2
...     yield 3
...
>>> gen = test()
>>> next(gen)
1
>>> gen.send("abc")
abc
2
>>> gen.throw(Exception())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in test
Exception

Sur appel de gen.send(), la valeur est passée comme une valeur de retour à partir de l' yield mot-clé.

gen.throw() d'autre part, permet de lever des Exceptions à l'intérieur de groupes électrogènes, l'exception soulevée à la même place, yield a été appelé.

Renvoi de valeurs à partir de générateurs de

Retour d'une valeur à partir d'un générateur, les résultats de la valeur d'être mis à l'intérieur de l' StopIteration d'exception. On peut par la suite de récupérer la valeur de l'exception et de l'utiliser à notre besoin.

>>> def test():
...     yield 1
...     return "abc"
...
>>> gen = test()
>>> next(gen)
1
>>> try:
...     next(gen)
... except StopIteration as exc:
...     print(exc.value)
...
abc

Voici, un nouveau mot-clé: yield from

Python 3.4 est venu avec l'ajout d'un nouveau mot-clé: yield from. Ce mot clé permet de le faire est de passer sur n'importe quel next(), send() et throw() dans un intérieur plus imbriqués générateur. Si l'intérieur du générateur renvoie une valeur, c'est également la valeur de retour de l' yield from:

>>> def inner():
...     print((yield 2))
...     return 3
...
>>> def outer():
...     yield 1
...     val = yield from inner()
...     print(val)
...     yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen)
2
>>> gen.send("abc")
abc
3
4

Mettre tous ensemble

Lors de l'introduction d'un nouveau mot-clé yield from en Python 3.4, nous sommes désormais en mesure de créer des générateurs à l'intérieur des générateurs que, comme dans un tunnel, de transmettre les données en arrière à partir de la plus intérieur à l'extérieur-la plupart des générateurs. Cela a donné naissance à un nouveau sens pour les générateurs - coroutines.

Coroutines sont des fonctions qui peuvent être arrêté et repris tout en cours d'exécution. En Python, qu'ils sont définis à l'aide de l' async def mot-clé. Beaucoup, comme les générateurs, ils utilisent leur propre forme d' yield from qui await. Avant d' async et await ont été introduites en Python 3.5, nous avons créé des coroutines exactement de la même façon générateurs ont été créés (avec yield from au lieu de await).

async def inner():
    return 1

async def outer():
    await inner()

Comme chaque itérateur ou générateur de mettre en œuvre l' __iter__() méthode, mettre en œuvre des coroutines __await__() , ce qui leur permet de continuer à chaque fois await coro est appelé.

Il y a un joli diagramme de séquence à l'intérieur de l' Python docs que vous devriez vérifier.

Dans asyncio, en dehors de coroutine fonctions, nous avons 2 objets importants: les tâches et les contrats à terme.

Les contrats à terme

Les contrats à terme sont des objets qui ont l' __await__() méthode de mise en œuvre, et leur mission est de maintenir un certain état et le résultat. L'état peut être l'un des suivants:

  1. Dans l'ATTENTE - l'avenir n'a pas de résultat ou d'une exception visée.
  2. ANNULÉ - l'avenir a été annulés à l'aide d' fut.cancel()
  3. TERMINÉ - l'avenir était fini, que ce soit par un résultat en utilisant fut.set_result() ou par une exception définie à l'aide de fut.set_exception()

Le résultat, comme vous l'avez deviné, peut être soit un objet Python, qui seront retournés, ou d'une exception qui peut être soulevée.

Une autre importante caractéristique de l' future objets, c'est qu'ils contiennent une méthode appelée add_done_callback(). Cette méthode permet des fonctions à être appelé dès que la tâche est terminée - si elle a soulevé une exception ou fini.

Tâches

Tâche les objets sont à terme spécial, qui s'enroulent autour de coroutines, et de communiquer avec le plus intérieur, l'extérieur et la plupart des coroutines. Chaque fois qu'une coroutine awaits a l'avenir, l'avenir est passé tout le chemin du retour à la tâche (tout comme dans l' yield from), et la tâche qu'il reçoit.

Ensuite, la tâche se lie lui-même à l'avenir. Elle le fait en appelant add_done_callback() sur l'avenir. À partir de maintenant, si l'avenir ne sera jamais fait, soit par l'annulation, passé une exception ou le passé d'un objet Python en conséquence, la tâche de rappel sera appelé, et il va remonter jusqu'à l'existence.

Asyncio

Le final de la gravure question à laquelle nous devons répondre est - comment est le IO mis en œuvre?

Profondément à l'intérieur de asyncio, nous avons une boucle d'événements. Une boucle de tâches. La boucle d'événement de l'emploi est d'appeler les tâches à chaque fois qu'ils sont prêts et de coordonner tous les efforts dans une seule machine de travail.

L'OI partie de la boucle d'événement est construit sur un seul appel de fonction select. Select est une fonction de blocage, mis en œuvre par le système d'exploitation dessous, qui permet d'attente sur des sockets pour les données entrantes ou sortantes. Sur les données en cours de réception, il se réveille et retourne les sockets qui a reçu les données, ou les sockets qui sont prêts pour l'écriture.

Lorsque vous essayez d'envoyer ou recevoir des données sur une socket par asyncio, ce qui se passe réellement en dessous, c'est que le socket est d'abord vérifié si il a pas de données qui peut être lu immédiatement envoyé. Si c'est .send() de la mémoire tampon est pleine, ou l' .recv() tampon est vide, le socket est inscrit à l' select de la fonction (en ajoutant simplement à l'une des listes, rlist pour recv et wlist pour send) et la fonction appropriée awaits nouvellement créé, future objet, lié à cette prise.

Lorsque toutes les tâches disponibles sont en attente pour l'avenir, la boucle d'événement appels select et attend. Lorsque l'une des prises de courant entrant les données, ou c'est send tampon drainé jusqu', asyncio vérifie pour l'avenir de l'objet lié à cette prise, et il définit à fait.

Maintenant, toute la magie se produit. L'avenir est réglé en fait, la tâche qui a ajouté de lui-même avant avec add_done_callback() s'élève jusqu'à revenir à la vie, et des appels .send() sur la coroutine qui reprend à l'intérieur de la plupart des coroutine (en raison de l' await chaîne) et vous lisez le nouveau les données reçues à partir d'une proximité de la mémoire tampon, il a été renversé à.

La méthode de la chaîne de nouveau, dans le cas d' recv():

  1. select.select attend.
  2. Un prêt socket, avec les données retournées.
  3. Les données de la socket est déplacé dans une mémoire tampon.
  4. future.set_result() est appelé.
  5. La tâche qui a ajouté de lui-même avec add_done_callback() est maintenant réveillé.
  6. La tâche des appels .send() sur la coroutine qui va tout le chemin à l'intérieur de la plupart des coroutine et de veille.
  7. La lecture de données à partir de la mémoire tampon et est retourné à notre humble utilisateur.

En résumé, asyncio utilise le générateur de capacités, qui permettent la suspension et la reprise de fonctions. Il utilise yield from des fonctionnalités qui permettent la transmission des données en arrière à partir de l'intérieur de la plupart générateur à l'extérieur-plus. Il utilise toutes ces afin de stopper l'exécution de la fonction tandis qu'il attend IO pour compléter (en utilisant le système d'exploitation select de la fonction).

Et le meilleur de tous? Tandis qu'une fonction est en pause, une autre peut exécuter et entrelacé avec le tissu délicat qui est asyncio.

18voto

user4815162342 Points 27348

Votre coro desugaring est théoriquement correct, mais un peu incomplet.

await ne pas suspendre de façon inconditionnelle, mais uniquement si elle rencontre un appel de blocage. Comment sait-il qu'un appel est bloquant? Cela est décidé par le code attendu. Par exemple, un awaitable mise en œuvre de la prise en lecture pourrait être délactosé à:

def read(sock, n):
    # sock must be in non-blocking mode
    try:
        return sock.recv(n)
    except EWOULDBLOCK:
        event_loop.add_reader(sock.fileno, current_task())
        return SUSPEND

Dans la vraie asyncio le code équivalent modifie l'état d'un Future au lieu de retourner la magie des valeurs, mais le concept est le même. Quand il est bien adapté à un générateur-comme l'objet, le code ci-dessus peuvent être awaited.

Sur le côté de l'appelant, lorsque votre coroutine contient:

data = await read(sock, 1024)

Il desugars en quelque chose de proche:

data = read(sock, 1024)
if data is SUSPEND:
    return SUSPEND
self.pos += 1
self.parts[self.pos](...)

Les personnes familières avec les générateurs ont tendance à décrire ci-dessus en termes de yield from qui ne la suspension automatiquement.

La suspension de la chaîne continue tout le chemin jusqu'à la boucle d'événements, qui remarque que la coroutine est suspendu, l'enlève de l'exécutable ensemble, et continue à exécuter des coroutines qui sont exécutables, le cas échéant. Si pas de coroutines sont praticable, la boucle attend en select() jusqu'à un descripteur de fichier une coroutine est intéressé à elle est prête à IO. (La boucle d'événements maintient un fichier de descripteur de-à-coroutine cartographie.)

Dans l'exemple ci-dessus, une fois select() raconte la boucle d'événements qu' sock est lisible, il sera re-ajouter coro de l'exécutable, de sorte qu'il sera poursuivi à partir du point de suspension.

En d'autres termes:

  1. Tout se passe dans le même thread par défaut.

  2. La boucle d'événements est responsable de la planification de la coroutines et de se réveiller quand tout ce qu'ils attendaient (généralement un IO appel qui serait normalement bloc, ou un délai d'attente) est prête.

Pour en savoir plus sur coroutine-la conduite d'une boucle d'événement, je recommande ce parler par Dave Beazley, où il démontre codage d'une boucle d'événement à partir de zéro en face de l'auditoire.

6voto

Vincent Points 1135

Tout se résume à les deux principaux défis que asyncio aborde les éléments suivants:

  • Comment effectuer des e/S multiples dans un seul thread?
  • Comment mettre en œuvre le multitâche coopératif?

La réponse au premier point a été autour pendant un long moment et est appelé sélectionnez boucle. En python, il est mis en œuvre dans les sélecteurs de module.

La deuxième question est liée à la notion de coroutine, c'est à dire des fonctions qui peuvent arrêter leur exécution et être restauré plus tard. En python, les coroutines sont mis en œuvre à l'aide de générateurs et le rendement de la déclaration. C'est ce qui se cache derrière la async/await syntaxe.

Davantage de ressources à cette réponse.


EDIT: répondre à votre commentaire sur les goroutines:

L'équivalent le plus proche pour une goroutine dans asyncio est en fait pas une coroutine, mais une tâche (voir la différence dans la documentation). En python, une coroutine (ou un groupe) ne sait rien sur les concepts de boucle d'événement ou I/O. C'est tout simplement une fonction qui peut arrêter son exécution à l'aide de yield tout en gardant son état actuel, de sorte qu'il peut être restauré plus tard. L' yield from syntaxe permet le chaînage de façon transparente.

Maintenant, à l'intérieur d'un asyncio tâche, la coroutine à la base de la chaîne se termine toujours par céder un avenir. Ce futur alors des bulles jusqu'à la boucle d'événements, et est intégré dans l'intérieur de la machine. Lorsque l'avenir est fixé à fait par certains autres intérieure de rappel, la boucle d'événement peut restaurer la tâche par l'envoi de l'avenir de retour dans la coroutine de la chaîne.


EDIT: s'attaquer à certaines des questions dans votre message:

Comment I/O se produisent réellement dans ce scénario? Dans un thread séparé? Est l'ensemble de l'interprète suspendu et I/O qui se passe à l'extérieur de l'interprète?

Non, rien ne se passe dans un fil de discussion. I/O est toujours gérée par la boucle d'événements, principalement par le biais de descripteurs de fichiers. Toutefois, l'enregistrement de ces descripteurs de fichier est généralement caché par le haut niveau de coroutines, faire le sale boulot pour vous.

Qu'entend-on exactement par I/O? Si mon python procédure appelée C open() de la procédure, et il à son tour a envoyé interrompre à noyau, de l'abandonner à elle, comment ne interpréteur Python savoir à ce sujet et est en mesure de poursuivre l'exécution de certains autres code, alors que le code du noyau de l'I/O et jusqu'à ce qu'il se réveille le Python de la procédure qui a envoyé l'interrompre à l'origine? Comment peut-interpréteur Python en principe, être conscient de ce qui se passe?

Un I/O est tout blocage d'appel. Dans asyncio, toutes les opérations d'e/S doit passer par la boucle d'événements, parce que, comme vous l'avez dit, la boucle d'événement n'a aucun moyen de savoir qu'un blocage d'appel est effectuée dans certains code synchrone. Cela signifie que vous n'êtes pas censé utiliser un synchrones open dans le contexte d'une coroutine. Au lieu de cela, utiliser une bibliothèque dédiée tels aiofiles qui fournit une version asynchrone de l' open.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X