Serveur d'impression

Comprendre l'asyncio de Python | Journal Linux – Bien choisir son serveur d impression

Le 11 mars 2020 - 12 minutes de lecture

Comment commencer à utiliser asyncio de Python.

Plus tôt cette année, j'ai assisté à PyCon, le salon international Python
conférence. Un sujet, présenté lors de nombreuses conférences et discuté
officieusement dans le couloir, était l'état de filetage en Python – qui
n'est, en un mot, ni idéal ni aussi terrible que certains critiques
se disputer.

Un sujet connexe qui a été soulevé à plusieurs reprises était celui de "l'asyncio", un
approche relativement nouvelle de la concurrence en Python. Non seulement il y avait
des présentations formelles et des discussions informelles sur l'asyncio, mais
nombre de personnes m'ont également posé des questions sur les cours sur le sujet.

Je dois admettre que j'ai été un peu surpris par tout l'intérêt. Après
tous, asyncio n'est pas un nouvel ajout à Python; ça fait un moment
quelques années. Et, cela ne résout pas tous les problèmes associés à
fils. De plus, il peut être déroutant pour de nombreuses personnes de commencer avec cela.

Et pourtant, on ne peut nier qu'après un certain nombre d'années où
les gens ont ignoré l'asyncio, ça commence à prendre de l'ampleur. Je suis sûr
en partie parce que l'asyncio a mûri et s'est amélioré au fil du temps,
merci en grande partie au travail dévoué de nombreux développeurs.
Mais c'est aussi parce que l'asyncio est un choix de plus en plus bon et utile
pour certains types de tâches, en particulier les tâches qui
les réseaux.

Donc, avec cet article, je lance une série sur asyncio – ce que c'est, comment
l'utiliser, où cela est approprié, et comment vous pouvez et devez (et ne pouvez pas non plus
et ne devrait pas) l'intégrer dans votre propre travail.

Qu'est-ce que l'asyncio?

Tout le monde est habitué à ce que les ordinateurs soient capables de faire plus d'une chose à la fois.
le temps – enfin, en quelque sorte. Bien qu’il puisse sembler que les ordinateurs sont
faire plus d'une chose à la fois, ils sont en train de changer, très
rapidement, à travers différentes tâches. Par exemple, lorsque vous ssh dans un Linux
serveur, il peut sembler qu'il n'exécute que vos commandes. Mais
en réalité, vous obtenez une petite "tranche de temps" du CPU, avec le
reste aller à d'autres tâches sur l'ordinateur, tels que les systèmes qui
gérer la mise en réseau, la sécurité et divers protocoles. En effet, si vous êtes
en utilisant SSH pour se connecter à un tel serveur, certaines de ces tranches de temps
sont utilisés par sshd pour gérer votre connexion et même vous permettre de
émettre des commandes.

Tout cela se fait, sur les systèmes d'exploitation modernes, via "préemption
multitâche ". En d'autres termes, les programmes en cours d'exécution n'ont pas le choix
quand ils abandonneront le contrôle du CPU. Ils sont plutôt obligés de
abandonnez le contrôle et reprenez un peu plus tard. Chaque processus
exécuté sur un ordinateur est géré de cette façon. Chaque processus peut, à son tour,
utiliser des threads, des sous-processus qui subdivisent la tranche de temps donnée à leur
processus parent.

Donc, sur un ordinateur hypothétique avec cinq processus (et un cœur), chacun
processus obtiendrait environ 20% du temps. Si l'un de ces processus était
pour avoir quatre threads, chaque thread obtiendrait 5% du temps du processeur.
(Les choses sont évidemment plus complexes que cela, mais c'est un bon moyen de
pensez-y à un niveau élevé.)

Python fonctionne très bien avec les processus via le "multiprocessing"
bibliothèque. Le problème avec les processus est qu'ils sont relativement grands et
encombrants, et vous ne pouvez pas les utiliser pour certaines tâches, telles que l'exécution d'un
fonction en réponse à un clic de bouton, tout en gardant l'interface utilisateur réactive.

Donc, vous voudrez peut-être utiliser des threads. Et en effet, les threads de Python fonctionnent,
et ils fonctionnent bien, pour de nombreuses tâches. Mais ils ne sont pas aussi bons qu'ils pourraient l'être,
à cause du GIL (le verrou d'interpréteur global), qui garantit que
un seul thread s'exécute à la fois. Alors bien sûr, Python vous laissera courir
programmes multithread, et ceux-là même fonctionnent bien quand ils sont
faire beaucoup d'E / S. C'est parce que les E / S sont lentes par rapport au CPU et
mémoire, et Python peut en profiter pour desservir d'autres threads.
Si vous utilisez des threads pour effectuer des calculs sérieux,
Les threads de Python sont une mauvaise idée, et ils ne vous mèneront nulle part. Même avec
de nombreux cœurs, un seul thread s'exécutera à la fois, ce qui signifie que vous êtes
rien de mieux que d'exécuter vos calculs en série.

Les ajouts asyncio à Python offrent un modèle différent pour la concurrence.
Comme pour les threads, asyncio n'est pas une bonne solution aux problèmes liés au processeur
(c'est-à-dire, qui ont besoin de beaucoup de temps CPU pour effectuer les calculs).
Cela ne convient pas non plus lorsque vous devez absolument que les choses fonctionnent vraiment
en parallèle, comme cela arrive avec les processus.

Mais si vos programmes fonctionnent avec le réseau, ou s'ils font des E / S étendues,
asyncio pourrait bien être une bonne façon de procéder.

La bonne nouvelle est que si cela est approprié, asyncio peut être beaucoup plus facile à
travailler avec des fils.

La mauvaise nouvelle est que vous devrez penser d'une manière nouvelle et différente au travail
avec asyncio.

Multitâche coopératif et coroutines

Plus tôt, j'ai mentionné que les systèmes d'exploitation modernes utilisent
multitâche "pour faire avancer les choses, forçant les processus à abandonner le contrôle
du CPU en faveur d'un autre processus. Mais il y a un autre modèle, connu
comme "multitâche coopératif", dans lequel le système attend qu'un programme
abandonne volontairement le contrôle du CPU. D'où le mot «coopération» – si la fonction a décidé d'effectuer des tas de calculs, et jamais
abandonne le contrôle, alors le système ne peut rien y faire.

Cela ressemble à une recette pour un désastre; pourquoi voudriez-vous écrire, encore moins
exécuter, des programmes qui abandonnent le CPU? La réponse est simple. Quand ton
programme utilise les E / S, vous pouvez à peu près garantir que vous serez
attendre paresseusement jusqu'à ce que vous obteniez une réponse, compte tenu du ralentissement des E / S
est que les programmes exécutés en mémoire. Ainsi, vous pouvez renoncer volontairement
CPU chaque fois que vous faites quelque chose avec les E / S, sachant que assez tôt, d'autres
De même, les programmes invoqueront les E / S et abandonneront le processeur, renvoyant
contrôle pour vous.

Pour que cela fonctionne, vous aurez besoin de tous les programmes
au sein de cet univers multitâche coopératif pour convenir d'un terrain
règles. En particulier, vous en aurez besoin pour accepter que toutes les E / S vont
à travers le système multitâche, et qu'aucune des tâches ne monopolisera le
CPU pendant une longue période de temps.

Mais attendez, vous en aurez également besoin d'un peu plus. Vous devrez donner aux tâches un moyen de
arrêtez de l'exécuter volontairement pendant un petit moment, puis redémarrez d'où
ils se sont arrêtés.

Ce dernier morceau existe en fait en Python depuis un certain temps, mais avec
syntaxe légèrement différente. Commençons le voyage
et l'exploration de l'asyncio là-bas.

Une fonction Python normale, lorsqu'elle est appelée, s'exécute du début à la fin.
Par exemple:





def foo ():
    impression ("a")
    impression ("b")
    impression ("c")

Si vous appelez cela, vous verrez:





une
b
c

Bien sûr, il est généralement bon pour les fonctions non seulement d'imprimer
quelque chose, mais aussi pour retourner une valeur:





def bonjour (nom):
    retourner f'Bonjour, nom '

Maintenant, lorsque vous appelez la fonction, vous récupérez quelque chose. Vous pouvez saisir
qui a renvoyé la valeur et l'assignez à une variable:





s = bonjour ('Reuven')

Mais il y a une variation sur revenir qui se révélera au cœur de ce
vous faites ici, à savoir rendement. le rendement la déclaration ressemble et agit
un peu comme revenir, mais il peut être utilisé plusieurs fois dans une fonction,
même dans une boucle:





def bonjour (nom):
    pour i dans la plage (5):
        rendement f '[i] Bonjour, name '

Parce qu'il utilise rendement, plutôt que revenir, c'est ce qu'on appelle un
"fonction générateur". Et quand vous l'invoquez, vous ne récupérez pas un
chaîne, mais plutôt un Générateur objet:





>>> g = bonjour ('Reuven')
>>> type (g)
Générateur

UNE Générateur est une sorte d'objet qui sait se comporter à l'intérieur d'un
Python pour boucle. (En d'autres termes, il implémente le protocole d'itération.)

Lorsqu'elle est placée dans une telle boucle, la fonction commence à s'exécuter. cependant,
chaque fois que la fonction de générateur rencontre un rendement déclaration, il
retourner la valeur à la boucle et se mettre en veille. Quand se réveille-t-il
encore? Quand le pour la boucle demande que la prochaine valeur soit retournée
l'itérateur:





pour s in g:
    impression (s)

Les fonctions de générateur fournissent ainsi l'essentiel de ce dont vous avez besoin: a
fonction qui s'exécute normalement, jusqu'à ce qu'elle atteigne un certain point dans le code.
À ce stade, il renvoie une valeur à son appelant et se met en veille. Quand
le pour boucle demande la prochaine valeur au générateur, la fonction
continue à s'exécuter là où il s'était arrêté (c'est-à-dire juste après la
rendement
comme si elle ne s'était jamais arrêtée.

Le fait est que les générateurs tels que décrits ici produisent une sortie, mais ne peuvent pas
obtenir n'importe quelle entrée. Par exemple, vous pouvez créer un générateur pour en renvoyer un
Nombre de Fibonacci par itération, mais vous ne pouvez pas lui dire de sauter dix
chiffres à venir. Une fois que la fonction générateur est en cours d'exécution, elle ne peut pas
entrées de l'appelant.

Il ne peut pas obtenir de telles entrées via le protocole d'itération normal, c'est-à-dire.
Les générateurs prennent en charge un envoyer méthode, permettant au monde extérieur d'envoyer
tout objet Python au générateur. De cette façon, les générateurs prennent désormais en charge
communication bidirectionnelle. Par exemple:





def bonjour (nom):
    tandis que True:
        name = yield f'Bonjour, name '
        sinon nom:
            Pause

Étant donné la fonction de générateur ci-dessus, vous pouvez maintenant dire:





>>> g = bonjour ('monde')

>>> suivant (g)
'Bonjour le monde'

>>> g.send ('Reuven')
«Bonjour, Reuven»

>>> g.send ('Linux Journal')
«Bonjour, Linux Journal»

En d'autres termes, vous exécutez d'abord la fonction de générateur pour obtenir un générateur
objet ("g") en arrière. Vous devez ensuite l'amorcer avec le suivant une fonction,
jusqu'à et y compris le premier rendement déclaration. À partir de ce
pointez sur, vous pouvez soumettre toute valeur que vous voulez au générateur via le
envoyer méthode. Jusqu'à ce que vous couriez g.send (Aucun), vous continuerez à recevoir
sortie en arrière.

Utilisé de cette façon, le générateur est appelé "coroutine", c'est-à-dire qu'il
a l'état et s'exécute. Mais, il s'exécute en tandem avec le principal
routine, et vous pouvez l'interroger chaque fois que vous voulez en tirer quelque chose.

L'asyncio de Python utilise ces concepts de base, quoique légèrement
syntaxe différente, pour atteindre ses objectifs. Et même si cela peut sembler
une chose banale pour pouvoir envoyer des données dans des générateurs, et obtenir
les choses reviennent régulièrement, c'est loin d'être le cas. En effet, cette
fournit le cœur d'une infrastructure complète qui vous permet de
créer des applications réseau efficaces pouvant gérer de nombreuses applications simultanées
utilisateurs, sans la douleur des threads ou des processus.

Dans mon prochain article, je prévois de commencer à regarder la syntaxe spécifique d'Asyncio et comment elle
correspond à ce que j'ai montré ici. Restez à l'écoute.

Commentaires

Laisser un commentaire

Votre commentaire sera révisé par les administrateurs si besoin.