Cette question est motivée par mon autre question: comment attendre en cdef?
Il y a des tonnes d'articles et de billets de blog sur le Web asyncio
, mais ils sont tous très superficiels. Je n'ai trouvé aucune information sur la manière dont asyncio
est réellement implémentée et sur ce qui rend les E / S asynchrones. J'essayais de lire le code source, mais ce sont des milliers de lignes de code C qui n'est pas de la plus haute qualité, dont beaucoup traitent des objets auxiliaires, mais surtout, il est difficile de se connecter entre la syntaxe Python et le code C qu'elle traduirait dans.
La propre documentation d'Asycnio est encore moins utile. Il n'y a aucune information sur son fonctionnement, seulement quelques directives sur la façon de l'utiliser, qui sont aussi parfois trompeuses / très mal rédigées.
Je connais l'implémentation des coroutines par Go et j'espérais en quelque sorte que Python fasse la même chose. Si tel était le cas, le code que je suis venu dans le post lié ci-dessus aurait fonctionné. Puisque ce n'est pas le cas, j'essaie maintenant de comprendre pourquoi. Ma meilleure estimation à ce jour est la suivante, veuillez me corriger là où je me trompe:
- Les définitions de procédure du formulaire
async def foo(): ...
sont en fait interprétées comme des méthodes d'héritage de classecoroutine
. - Peut-être,
async def
est en fait divisé en plusieurs méthodes par desawait
instructions, où l'objet, sur lequel ces méthodes sont appelées, est capable de suivre la progression de l'exécution jusqu'à présent. - Si ce qui précède est vrai, alors, essentiellement, l'exécution d'une coroutine se résume à appeler des méthodes d'objet coroutine par un gestionnaire global (boucle?).
- Le gestionnaire global est en quelque sorte (comment?) Conscient du moment où les opérations d'E / S sont effectuées par du code Python (uniquement?) Et est capable de choisir l'une des méthodes de coroutine en attente à exécuter après que la méthode d'exécution actuelle a abandonné le contrôle (appuyez sur l'
await
instruction ).
En d'autres termes, voici ma tentative de "désuétiser" une asyncio
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 est correcte: alors j'ai un problème. Comment les E / S se produisent-elles réellement dans ce scénario? Dans un fil séparé? L'interprète entier est-il suspendu et les E / S se produisent en dehors de l'interprète? Qu'entend-on exactement par E / S? Si ma procédure python a appelé procédure C open()
, et qu'elle a à son tour envoyé une interruption au noyau, lui abandonnant le contrôle, comment l'interpréteur Python sait-il à ce sujet et est-il capable de continuer à exécuter un autre code, tandis que le code du noyau effectue les E / S réelles et jusqu'à ce que il réveille la procédure Python qui a envoyé l'interruption à l'origine? Comment l'interpréteur Python peut-il en principe être conscient de ce qui se passe?
BaseEventLoop
est implémenté: github.com/python/cpython/blob/…_run_once
, qui est en fait la seule fonction utile de tout ce module est-il rendu "privé"? La mise en œuvre est horrible, mais c'est moins un problème. Pourquoi la seule fonction que vous voudriez appeler sur une boucle d'événement est marquée comme "ne m'appelle pas"?_run_once
en premier lieu?_run_once
?asyncio
est complexe et a ses défauts, mais veuillez garder la discussion civile. Ne blâmez pas les développeurs derrière un code que vous ne comprenez pas vous-même.Réponses:
Comment fonctionne asyncio?
Avant de répondre à cette question, nous devons comprendre quelques termes de base, sautez-les si vous en connaissez déjà un.
Générateurs
Les générateurs sont des objets qui nous permettent de suspendre l'exécution d'une fonction python. Les générateurs sélectionnés par l'utilisateur sont implémentés à l'aide du mot-clé
yield
. En créant une fonction normale contenant leyield
mot - clé, nous transformons cette fonction en générateur:Comme vous pouvez le voir, l'appel
next()
au générateur amène l'interpréteur à charger la trame du test et à renvoyer layield
valeur ed. Unnext()
nouvel appel provoque le chargement de la trame dans la pile d'interpréteur et continueyield
une autre valeur.Au troisième
next()
appel, notre générateur était terminé et aStopIteration
été lancé.Communiquer avec un générateur
Une caractéristique moins connue des générateurs est le fait que vous pouvez communiquer avec eux en utilisant deux méthodes:
send()
etthrow()
.Lors de l'appel
gen.send()
, la valeur est transmise comme valeur de retour duyield
mot - clé.gen.throw()
d'autre part, permet de lancer des exceptions à l'intérieur des générateurs, à l'exception soulevée au même endroit ayield
été appelée.Renvoyer les valeurs des générateurs
Le renvoi d'une valeur à partir d'un générateur entraîne la mise de la valeur dans l'
StopIteration
exception. Nous pouvons plus tard récupérer la valeur de l'exception et l'utiliser selon nos besoins.Voici, un nouveau mot-clé:
yield from
Python 3.4 est venu avec l'ajout d'un nouveau mot - clé:
yield from
. Qu'est - ce que ce mot - clé nous permet de faire, est de passer sur toutnext()
,send()
etthrow()
dans un générateur interne le plus imbriqué. Si le générateur interne renvoie une valeur, c'est aussi la valeur de retour deyield from
:J'ai écrit un article pour approfondir ce sujet.
Mettre tous ensemble
Lors de l'introduction du nouveau mot-clé
yield from
dans Python 3.4, nous étions maintenant en mesure de créer des générateurs à l'intérieur de générateurs qui, tout comme un tunnel, transmettent les données dans les deux sens des générateurs les plus internes aux générateurs les plus externes. Cela a engendré une nouvelle signification pour les générateurs - les coroutines .Les coroutines sont des fonctions qui peuvent être arrêtées et reprises pendant leur exécution. En Python, ils sont définis à l'aide du
async def
mot - clé. Tout comme les générateurs, ils utilisent aussi leur propre formeyield from
dont estawait
. Avantasync
etawait
avons été introduits dans Python 3.5, nous avons créé des coroutines exactement de la même manière que les générateurs étaient créés (avecyield from
au lieu deawait
).Comme tous les itérateurs ou générateurs qui implémentent la
__iter__()
méthode, les coroutines implémentent__await__()
ce qui leur permet de continuer à chaqueawait coro
appel.Il y a un joli diagramme de séquence dans la documentation Python que vous devriez consulter.
Dans asyncio, outre les fonctions coroutines, nous avons 2 objets importants: les tâches et les futurs .
Futures
Les futurs sont des objets dont la
__await__()
méthode est implémentée, et leur travail consiste à conserver un certain état et un certain résultat. L'état peut être l'un des suivants:fut.cancel()
fut.set_result()
ou par un jeu d'exceptions utilisantfut.set_exception()
Le résultat, comme vous l'avez deviné, peut être soit un objet Python, qui sera retourné, soit une exception qui peut être déclenchée.
Une autre caractéristique importante des
future
objets est qu'ils contiennent une méthode appeléeadd_done_callback()
. Cette méthode permet d'appeler des fonctions dès que la tâche est terminée - qu'elle ait déclenché une exception ou qu'elle soit terminée.Tâches
Les objets de tâche sont des futurs spéciaux, qui s'enroulent autour des coroutines et communiquent avec les coroutines les plus internes et les plus externes. Chaque fois qu'une coroutine est
await
un futur, le futur est renvoyé à la tâche (comme dansyield from
), et la tâche le reçoit.Ensuite, la tâche se lie à l'avenir. Il le fait en invoquant
add_done_callback()
l'avenir. À partir de maintenant, si le futur sera jamais fait, soit en étant annulé, en passant une exception ou en passant un objet Python en conséquence, le rappel de la tâche sera appelé et il reviendra à l'existence.Asyncio
La dernière question brûlante à laquelle nous devons répondre est la suivante: comment l'OI est-il mis en œuvre?
Au plus profond d'Asyncio, nous avons une boucle d'événements. Une boucle d'événements de tâches. Le travail de la boucle d'événements est d'appeler des tâches chaque fois qu'elles sont prêtes et de coordonner tous ces efforts dans une seule machine de travail.
La partie IO de la boucle d'événements repose sur une seule fonction cruciale appelée
select
. Select est une fonction de blocage, implémentée par le système d'exploitation en dessous, qui permet d'attendre sur les sockets des données entrantes ou sortantes. Lors de la réception des données, il se réveille et renvoie les sockets qui ont reçu des données, ou les sockets qui sont prêtes pour l'écriture.Lorsque vous essayez de recevoir ou d'envoyer des données sur un socket via asyncio, ce qui se passe en fait ci-dessous est que le socket est d'abord vérifié s'il contient des données pouvant être immédiatement lues ou envoyées. Si son
.send()
tampon est plein ou si le.recv()
tampon est vide, le socket est enregistré dans laselect
fonction (en l'ajoutant simplement à l'une des listes,rlist
pourrecv
etwlist
poursend
) et la fonction appropriée estawait
unfuture
objet nouvellement créé , lié à ce socket.Lorsque toutes les tâches disponibles attendent des futurs, l'événement appelle
select
et attend. Lorsque l'une des sockets a des données entrantes ou que sasend
mémoire tampon est épuisée, asyncio vérifie le futur objet lié à cette socket et le définit sur done.Maintenant, toute la magie opère. L'avenir est prêt à être terminé, la tâche qui s'est ajoutée avant avec
add_done_callback()
remonte à la vie, et appelle.send()
la coroutine qui reprend la coroutine la plus interne (à cause de laawait
chaîne) et vous lisez les données nouvellement reçues à partir d'un tampon proche. a été renversé.Chaîne de méthodes à nouveau, en cas de
recv()
:select.select
attend.future.set_result()
est appelé.add_done_callback()
est maintenant réveillée..send()
la coroutine qui va jusqu'à la coroutine la plus interne et la réveille.En résumé, asyncio utilise des capacités de générateur, qui permettent de mettre en pause et de reprendre les fonctions. Il utilise des
yield from
capacités qui permettent de passer des données dans les deux sens du générateur le plus interne au générateur le plus externe. Il utilise tout cela pour arrêter l'exécution de la fonction pendant qu'il attend la fin des E / S (en utilisant laselect
fonction OS ).Et le meilleur de tous? Pendant qu'une fonction est en pause, une autre peut s'exécuter et s'entrelacer avec le tissu délicat, qui est asyncio.
la source
yield from
fonctionnement des générateurs . J'ai cependant noté en haut qu'il est désactivable au cas où le lecteur le saurait déjà :-) Y a-t-il autre chose que vous pensez que je devrais ajouter?select
peut également être qualifié, car c'est ainsi que les appels système d'E / S non bloquants fonctionnent sur le système d'exploitation. Lesasyncio
constructions réelles et la boucle d'événements ne sont que du code au niveau de l'application construit à partir de ces éléments.Parler
async/await
et ceasyncio
n'est pas la même chose. La première est une construction fondamentale de bas niveau (coroutines) tandis que la dernière est une bibliothèque utilisant ces constructions. À l'inverse, il n'y a pas de réponse ultime unique.Ce qui suit est une description générale de la façon
async/await
et leasyncio
travail des bibliothèques -comme. Autrement dit, il peut y avoir d'autres astuces en plus (il y en a ...) mais elles sont sans importance à moins que vous ne les construisiez vous-même. La différence devrait être négligeable sauf si vous en savez déjà assez pour ne pas avoir à poser une telle question.1. Coroutines versus sous-programmes dans une coquille de noix
Juste comme sous-programmes (fonctions, procédures, ...), les coroutines (générateurs, ...) sont une abstraction de pile d'appels et de pointeur d'instruction: il y a une pile de morceaux de code en cours d'exécution, et chacun est à une instruction spécifique.
La distinction entre
def
contreasync def
est simplement pour plus de clarté. La différence réelle estreturn
par rapport àyield
. À partir de là,await
ouyield from
faites la différence entre les appels individuels et des piles entières.1.1. Sous-programmes
Un sous-programme représente un nouveau niveau de pile pour contenir les variables locales, et un seul parcours de ses instructions pour atteindre une fin. Considérez un sous-programme comme celui-ci:
Lorsque vous l'exécutez, cela signifie
bar
etqux
return
, poussez sa valeur vers la pile appelanteNotamment, 4. signifie qu'un sous-programme démarre toujours au même état. Tout ce qui est exclusif à la fonction elle-même est perdu à la fin. Une fonction ne peut pas être reprise, même s'il y a des instructions après
return
.1.2. Les coroutines comme sous-programmes persistants
Une coroutine est comme un sous-programme, mais peut sortir sans détruire son état. Considérez une coroutine comme celle-ci:
Lorsque vous l'exécutez, cela signifie
bar
etqux
yield
, poussez sa valeur vers la pile appelante mais stockez la pile et le pointeur d'instructionyield
, restaurez le pointeur de pile et d'instruction et poussez les arguments versqux
return
, poussez sa valeur vers la pile appelanteNotez l'ajout de 2.1 et 2.2 - une coroutine peut être suspendue et reprise à des points prédéfinis. Ceci est similaire à la façon dont un sous-programme est suspendu lors de l'appel d'un autre sous-programme. La différence est que la coroutine active n'est pas strictement liée à sa pile appelante. Au lieu de cela, une coroutine suspendue fait partie d'une pile séparée et isolée.
Cela signifie que les coroutines suspendues peuvent être librement stockées ou déplacées entre les piles. Toute pile d'appels ayant accès à une coroutine peut décider de la reprendre.
1.3. Traverser la pile d'appels
Jusqu'à présent, notre coroutine descend uniquement dans la pile d'appels avec
yield
. Un sous-programme peut descendre et remonter la pile d'appels avecreturn
et()
. Pour être complet, les coroutines ont également besoin d'un mécanisme pour remonter la pile d'appels. Considérez une coroutine comme celle-ci:Lorsque vous l'exécutez, cela signifie qu'il alloue toujours le pointeur de pile et d'instruction comme un sous-programme. Quand il se suspend, c'est comme stocker un sous-programme.
Cependant,
yield from
fait les deux . Il suspend la pile et le pointeur d'instructionswrap
et s'exécutecofoo
. Notez quewrap
reste suspendu jusqu'à ce qu'il secofoo
termine complètement. Chaque fois quecofoo
suspend ou quelque chose est envoyé,cofoo
est directement connecté à la pile appelante.1.4. Coroutines tout en bas
Comme établi,
yield from
permet de connecter deux oscilloscopes sur un autre intermédiaire. Lorsqu'il est appliqué de manière récursive, cela signifie que le haut de la pile peut être connecté au bas de la pile.Notez cela
root
etcoro_b
ne vous connaissez pas. Cela rend les coroutines beaucoup plus propres que les callbacks: les coroutines sont toujours construites sur une relation 1: 1 comme les sous-programmes. Les coroutines suspendent et reprennent l'intégralité de leur pile d'exécution existante jusqu'à un point d'appel normal.Notamment,
root
pourrait avoir un nombre arbitraire de coroutines à reprendre. Pourtant, il ne peut jamais en reprendre plus d'un à la fois. Les coroutines de même racine sont concurrentes mais pas parallèles!1.5. Python
async
etawait
L'explication a jusqu'à présent utilisé explicitement le vocabulaire
yield
et lesyield from
générateurs - la fonctionnalité sous-jacente est la même. La nouvelle syntaxe Python3.5async
etawait
existe principalement pour plus de clarté.Les instructions
async for
etasync with
sont nécessaires car vous briseriez layield from/await
chaîne avec les instructionsfor
et nueswith
.2. Anatomie d'une simple boucle d'événements
En soi, une coroutine n'a aucun concept de céder le contrôle à une autre coroutine. Il ne peut céder le contrôle qu'à l'appelant au bas d'une pile de coroutine. Cet appelant peut alors passer à une autre coroutine et l'exécuter.
Ce nœud racine de plusieurs coroutines est généralement une boucle d'événement : en cas de suspension, une coroutine génère un événement sur lequel elle veut reprendre. À son tour, la boucle d'événements est capable d'attendre efficacement que ces événements se produisent. Cela lui permet de décider quelle coroutine exécuter ensuite, ou comment attendre avant de reprendre.
Une telle conception implique qu'il existe un ensemble d'événements prédéfinis que la boucle comprend. Plusieurs coroutines
await
, jusqu'à ce que finalement un événement soitawait
édité. Cet événement peut communiquer directement avec la boucle d'événementsyield
en contrôlant.La clé est que la suspension de coroutine permet à la boucle d'événements et aux événements de communiquer directement. La pile de coroutine intermédiaire ne nécessite pas aucune connaissance de la boucle qui l'exécute, ni du fonctionnement des événements.
2.1.1. Événements dans le temps
L'événement le plus simple à gérer atteint un point dans le temps. Il s'agit également d'un bloc fondamental de code threadé: un thread
sleep
s à plusieurs reprises jusqu'à ce qu'une condition soit vraie. Cependant, un habituésleep
exécution bloque par elle-même - nous voulons que les autres coroutines ne soient pas bloquées. Au lieu de cela, nous voulons dire à la boucle d'événements quand elle doit reprendre la pile de coroutine actuelle.2.1.2. Définition d'un événement
Un événement est simplement une valeur que nous pouvons identifier - que ce soit via une énumération, un type ou une autre identité. Nous pouvons définir cela avec une classe simple qui stocke notre temps cible. En plus de stocker les informations d'événement, nous pouvons autoriser
await
une classe directement.Cette classe ne stocke que l'événement - elle ne dit pas comment le gérer réellement.
La seule particularité est
__await__
- c'est ce que recherche leawait
mot - clé. En pratique, il s'agit d'un itérateur mais non disponible pour les machines d'itération régulières.2.2.1. En attente d'un événement
Maintenant que nous avons un événement, comment réagissent les coroutines? Nous devrions pouvoir exprimer l'équivalent de
sleep
parawait
notre événement. Pour mieux voir ce qui se passe, nous attendons deux fois la moitié du temps:Nous pouvons directement instancier et exécuter cette coroutine. Similaire à un générateur, l'utilisation
coroutine.send
exécute la coroutine jusqu'à ce qu'elle soityield
un résultat.Cela nous donne deux
AsyncSleep
événements et ensuite unStopIteration
lorsque la coroutine est terminée. Notez que le seul retard provient detime.sleep
la boucle! ChacunAsyncSleep
ne stocke qu'un décalage par rapport à l'heure actuelle.2.2.2. Événement + sommeil
À ce stade, nous avons deux mécanismes distincts à notre disposition:
AsyncSleep
Événements pouvant être générés depuis l'intérieur d'une coroutinetime.sleep
qui peut attendre sans impacter les coroutinesNotamment, ces deux sont orthogonaux: ni l'un n'affecte ni ne déclenche l'autre. En conséquence, nous pouvons proposer notre propre stratégie pour
sleep
faire face au retard d'unAsyncSleep
.2.3. Une boucle événementielle naïve
Si nous avons plusieurs coroutines, chacune peut nous dire quand elle veut être réveillée. On peut alors attendre que le premier d'entre eux veuille être repris, puis celui d'après, et ainsi de suite. Notamment, à chaque point, nous ne nous soucions que de celui qui est le suivant .
Cela permet une planification simple:
Une implémentation triviale ne nécessite aucun concept avancé. A
list
permet de trier les coroutines par date. L'attente est un habituétime.sleep
. L'exécution de coroutines fonctionne comme avant aveccoroutine.send
.Bien entendu, cela peut être amélioré. Nous pouvons utiliser un tas pour la file d'attente ou une table de répartition pour les événements. Nous pourrions également récupérer les valeurs de retour du
StopIteration
et les affecter à la coroutine. Cependant, le principe fondamental reste le même.2.4. Attente coopérative
L'
AsyncSleep
événement et larun
boucle d'événements sont une implémentation entièrement fonctionnelle des événements chronométrés.Cela commute en coopération entre chacune des cinq coroutines, en les suspendant chacune pendant 0,1 seconde. Même si la boucle d'événements est synchrone, elle exécute toujours le travail en 0,5 seconde au lieu de 2,5 secondes. Chaque coroutine détient un état et agit indépendamment.
3. Boucle d'événements d'E / S
Une boucle d'événements qui prend en charge
sleep
convient à l' interrogation . Cependant, l'attente d'E / S sur un descripteur de fichier peut se faire plus efficacement: le système d'exploitation implémente les E / S et sait donc quels descripteurs sont prêts. Idéalement, une boucle d'événement devrait prendre en charge un événement explicite «prêt pour les E / S».3.1. L'
select
appelPython a déjà une interface pour interroger le système d'exploitation pour les poignées d'E / S de lecture. Lorsqu'il est appelé avec des poignées pour lire ou écrire, il renvoie les poignées prêtes à lire ou à écrire:
Par exemple, nous pouvons
open
un fichier à écrire et attendre qu'il soit prêt:Une fois les retours sélectionnés,
writeable
contient notre fichier ouvert.3.2. Événement d'E / S de base
Comme pour la
AsyncSleep
demande, nous devons définir un événement pour les E / S. Avec laselect
logique sous-jacente , l'événement doit faire référence à un objet lisible - disons unopen
fichier. De plus, nous stockons la quantité de données à lire.Comme pour la
AsyncSleep
plupart, nous stockons simplement les données nécessaires à l'appel système sous-jacent. Cette fois, il__await__
est capable d'être repris plusieurs fois - jusqu'à ce que notre désiramount
ait été lu. De plus, nous obtenonsreturn
le résultat d'E / S au lieu de simplement reprendre.3.3. Augmenter une boucle d'événements avec des E / S de lecture
La base de notre boucle d'événements est toujours celle
run
définie précédemment. Tout d'abord, nous devons suivre les demandes de lecture. Ce n'est plus un planning trié, nous mappons uniquement les requêtes de lecture aux coroutines.Comme
select.select
prend un paramètre de délai d'attente, nous pouvons l'utiliser à la place detime.sleep
.Cela nous donne tous les fichiers lisibles - s'il y en a, nous exécutons la coroutine correspondante. S'il n'y en a pas, nous avons attendu assez longtemps pour que notre coroutine actuelle s'exécute.
Enfin, nous devons réellement écouter les demandes de lecture.
3.4. Mettre ensemble
Ce qui précède était un peu une simplification. Nous devons faire quelques changements pour ne pas affamer les coroutines endormies si nous pouvons toujours lire. Nous devons gérer n'avoir rien à lire ou rien à attendre. Cependant, le résultat final s'inscrit toujours dans 30 LOC.
3.5. E / S coopératives
Les implémentations
AsyncSleep
,AsyncRead
etrun
sont désormais entièrement fonctionnelles pour dormir et / ou lire. Comme poursleepy
, nous pouvons définir un assistant pour tester la lecture:En exécutant cela, nous pouvons voir que nos E / S sont entrelacées avec la tâche en attente:
4. E / S non bloquantes
Alors que les E / S sur les fichiers font passer le concept, cela ne convient pas vraiment à une bibliothèque comme
asyncio
: l'select
appel revient toujours pour les fichiers , et les deuxopen
etread
peuvent bloquer indéfiniment . Cela bloque toutes les coroutines d'une boucle d'événements - ce qui est mauvais. Les bibliothèques comme l'aiofiles
utilisation de threads et la synchronisation pour simuler des E / S et des événements non bloquants sur fichier.Cependant, les sockets permettent des E / S non bloquantes - et leur latence inhérente la rend beaucoup plus critique. Lorsqu'il est utilisé dans une boucle d'événements, l'attente de données et la nouvelle tentative peuvent être encapsulées sans rien bloquer.
4.1. Événement d'E / S non bloquant
Semblable à notre
AsyncRead
, nous pouvons définir un événement suspend-and-read pour les sockets. Au lieu de prendre un fichier, nous prenons une socket - qui doit être non bloquante. En outre, nos__await__
utilisationssocket.recv
au lieu defile.read
.Contrairement à
AsyncRead
,__await__
effectue des E / S vraiment non bloquantes. Lorsque les données sont disponibles, elles sont toujours lues. Lorsqu'aucune donnée n'est disponible, il est toujours suspendu. Cela signifie que la boucle d'événements n'est bloquée que pendant que nous effectuons un travail utile.4.2. Débloquer la boucle d'événements
En ce qui concerne la boucle d'événements, rien ne change beaucoup. L'événement à écouter est toujours le même que pour les fichiers - un descripteur de fichier marqué prêt par
select
.À ce stade, il devrait être évident que
AsyncRead
etAsyncRecv
sont le même genre d'événement. Nous pourrions facilement les refactoriser en un seul événement avec un composant d'E / S échangeable. En effet, la boucle d'événements, les coroutines et les événements séparent proprement un planificateur, un code intermédiaire arbitraire et les E / S réelles.4.3. Le côté laid des E / S non bloquantes
En principe, ce que vous devez faire à ce stade est de reproduire la logique de
read
as arecv
forAsyncRecv
. Cependant, c'est beaucoup plus laid maintenant - vous devez gérer les retours anticipés lorsque les fonctions se bloquent à l'intérieur du noyau, mais vous cèdent le contrôle. Par exemple, l'ouverture d'une connexion par rapport à l'ouverture d'un fichier est beaucoup plus longue:Pour faire court, il ne reste que quelques dizaines de lignes de gestion des exceptions. Les événements et la boucle d'événements fonctionnent déjà à ce stade.
Addenda
Exemple de code sur github
la source
yield self
dans AsyncSleep me donne uneTask got back yield
erreur, pourquoi? Je vois que le code dans asyncio.Futures l'utilise. L'utilisation d'un rendement nu fonctionne très bien.Votre
coro
désuétude est conceptuellement correcte, mais légèrement incomplète.await
ne suspend pas inconditionnellement, mais seulement s'il rencontre un appel bloquant. Comment sait-il qu'un appel est bloqué? Ceci est décidé par le code attendu. Par exemple, une implémentation attendue de socket read pourrait être déconseillée pour:En vrai asyncio, le code équivalent modifie l'état de a
Future
au lieu de renvoyer des valeurs magiques, mais le concept est le même. Lorsqu'il est adapté de manière appropriée à un objet de type générateur, le code ci-dessus peut êtreawait
édité.Côté appelant, lorsque votre coroutine contient:
Il se désucre en quelque chose de proche de:
Les personnes familières avec les générateurs ont tendance à décrire ce qui précède en termes de
yield from
qui effectue la suspension automatiquement.La chaîne de suspension continue jusqu'à la boucle d'événements, qui remarque que la coroutine est suspendue, la supprime de l'ensemble exécutable et continue d'exécuter des coroutines exécutables, le cas échéant. Si aucune coroutine n'est exécutable, la boucle attend
select()
jusqu'à ce qu'un descripteur de fichier auquel une coroutine s'intéresse devienne prêt pour IO. (La boucle d'événements maintient un mappage descripteur de fichier vers coroutine.)Dans l'exemple ci-dessus, une fois que
select()
la boucle d'événementssock
est lisible, elle sera rajoutéecoro
à l'ensemble exécutable, de sorte qu'elle se poursuivra à partir du point de suspension.En d'autres termes:
Tout se passe dans le même fil par défaut.
La boucle d'événements est chargée de planifier les coroutines et de les réveiller lorsque tout ce qu'ils attendaient (généralement un appel IO qui se bloquerait normalement ou un délai d'expiration) devient prêt.
Pour un aperçu des boucles d'événements de conduite de coroutine, je recommande cette conférence de Dave Beazley, où il montre le codage d'une boucle d'événements à partir de zéro devant un public en direct.
la source
async.wait_for()
ne fait pas ce qu'il est censé faire ... Pourquoi est-ce un si gros problème d'ajouter un rappel à la boucle d'événements et de le dire pour traiter le nombre de rappels dont il a besoin, y compris celui que vous venez d'ajouter? Ma frustration avecasyncio
est en partie due au fait que le concept sous-jacent est très simple et, par exemple, Emacs Lisp a été implémenté pendant des lustres, sans utiliser de mots à la mode ... (c'estcreate-async-process
-à- dire etaccept-process-output
- et c'est tout ce qu'il faut ... (suite)wait_for
cela ne fait pas ce qu'il est censé faire (cela fait, c'est une coroutine que vous êtes censé attendre), c'est que vos attentes ne correspondent pas à ce pour quoi le système a été conçu et implémenté. Je pense que votre problème pourrait être associé à asyncio si la boucle d'événements s'exécutait dans un thread séparé, mais je ne connais pas les détails de votre cas d'utilisation et, honnêtement, votre attitude ne rend pas très amusant de vous aider.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Rien ne vous empêche de mettre en œuvre ce concept simple sans mots à la mode pour le Python alors :) Pourquoi utilisez-vous ce vilain asyncio? Implémentez le vôtre à partir de zéro. Par exemple, vous pouvez commencer par créer votre propreasync.wait_for()
fonction qui fait exactement ce qu'elle est censée faire.asyncio
. Mais, en principe, ce n’est pas ma décision à prendre. Je suis contraint d'utiliser le langage des ordures via en.wikipedia.org/wiki/Ultimatum_game .Tout se résume aux deux principaux défis auxquels asyncio fait face:
La réponse au premier point existe depuis longtemps et s'appelle une boucle de sélection . En python, il est implémenté dans le module des sélecteurs .
La deuxième question est liée au concept de coroutine , c'est-à-dire des fonctions qui peuvent arrêter leur exécution et être restaurées ultérieurement. En python, les coroutines sont implémentées à l'aide de générateurs et de l' instruction yield from . C'est ce qui se cache derrière la syntaxe async / await .
Plus de ressources dans cette réponse .
EDIT: Répondre à votre commentaire sur les goroutines:
L'équivalent le plus proche d'un goroutine dans asyncio n'est en fait pas une coroutine mais une tâche (voir la différence dans la documentation ). En python, une coroutine (ou un générateur) ne sait rien des concepts de boucle d'événement ou d'E / S. Il s'agit simplement d'une fonction qui peut arrêter son exécution en utilisant
yield
tout en conservant son état actuel, afin qu'elle puisse être restaurée plus tard. Layield from
syntaxe permet de les chaîner de manière transparente.Désormais, dans une tâche asyncio, la coroutine tout en bas de la chaîne finit toujours par céder un avenir . Cet avenir bouillonne alors jusqu'à la boucle des événements et s'intègre dans la machinerie interne. Lorsque le futur est défini par un autre rappel interne, la boucle d'événements peut restaurer la tâche en renvoyant le futur dans la chaîne coroutine.
MODIFIER: Répondre à certaines des questions de votre message:
Non, rien ne se passe dans un fil. Les E / S sont toujours gérées par la boucle d'événements, principalement via des descripteurs de fichiers. Cependant, l'enregistrement de ces descripteurs de fichiers est généralement masqué par des coroutines de haut niveau, ce qui fait le sale boulot pour vous.
Une E / S est un appel bloquant. Dans asyncio, toutes les opérations d'E / S doivent passer par la boucle d'événements, car comme vous l'avez dit, la boucle d'événements n'a aucun moyen de savoir qu'un appel de blocage est effectué dans un code synchrone. Cela signifie que vous n'êtes pas censé utiliser un synchrone
open
dans le contexte d'une coroutine. À la place, utilisez une bibliothèque dédiée telle que aiofiles qui fournit une version asynchrone deopen
.la source
yield from
ne dit vraiment rien.yield from
est juste une construction syntaxique, ce n'est pas un élément fondamental que les ordinateurs peuvent exécuter. De même, pour la boucle de sélection. Oui, les coroutines dans Go utilisent également la boucle de sélection, mais ce que j'essayais de faire fonctionnerait dans Go, mais pas en Python. J'ai besoin de réponses plus détaillées pour comprendre pourquoi cela n'a pas fonctionné.asyncio
fait, pour moi, se résumerait à un code C qui illustre en quoi la syntaxe Python a été traduite.