Je vois beaucoup dans les tutos ici et là des gens qui opposent asyncio avec l’ancienne manière de faire de l’IO non bloquante : les threads.
C’est une erreur : les deux méthodes ne sont pas opposées, elles sont complémentaires.
En effet, asyncio ne peut être non bloquant que sur le réseau : ça ne gère pas l’IO sur les fichiers. Par ailleurs, toute opération CPU bloque également la boucle d’évènement.
C’est très bien de ne pas bloquer sur le réseau, mais encore faut-il pouvoir faire la requête réseau.
Si votre programme est bloqué à attendre une compression zip qui dure 3 secondes, pendant ce temps, les opérations réseaux déjà lancées tournent bien.
MAIS IL N’EN LANCERA PAS DE NOUVELLES.
Pendant 3 secondes, aucune nouvelle requête ne sera faite puisque le programme ne fait qu’une chose : ziper. Ca marche pour d’autres choses hein : copie de fichier, gros calcul matheux, traverser une grande liste, etc.
Donc si vous avez fini toutes vos requêtes à la seconde 1, pendant 2 secondes, votre programme n’est pas utilisé à son plein potentiel.
Les threads ne permettent pas d’avancer plus vite, mais ils permettent de faire 2 travaux en parallèle. Par exemple, de lancer pendant ces 2 secondes des requêtes supplémentaires sur le réseau.
On a vendu asyncio comme plus léger que les threads. Ce n’est pas tout à fait exact. asyncio a les avantages suivants :
Mais si on a quelques threads, le changement de contexte entre les threads est moins important que ce que coûte asyncio à faire tourner.
Pour certains travaux où le coût d’une opération réseau est faible, mais que cumulativement toutes les opérations ralentissent votre programme, un thread sera plus performant.
Par exemple, beaucoup d’opérations sur les bases de données tombent dans cette catégorie, à moins d’avoir des très longues requêtes.
Dans ce cas, avoir un thread dédié aux opérations de la base de données peut être une bonne décision.
Si vous avez des opérations sur des fichiers ou de grosses opérations CPU, les faire travailler dans un thread peut booster votre programme. Ca tombe bien il y a une lib pour ça.
Si vous avez beaucoup de petites opérations réseau, les grouper, dans un thread à part, peut booster votre programme.
Et asyncio pour le reste, par exemple des requêtes HTTPs, DNS, SMTP, FTP, SSH, etc.
On peut donc copieusement utiliser les deux en parallèle. La bonne nouvelle, c’est que Guido a designé asyncio pour ça:
import asyncio
import aiohttp
async def main(loop):
# ça c’est fait avec asyncio
await aiohttp.get('http://bidule.com')
# ça c’est fait dans un thread
await loop.run_in_executor(None, gros_calcul, params)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
Notez bien :
ProcessPoolExecutor
ou un ThreadPoolExecutor
pour choisir la stratégie de parallélisme à adapter : process, thread, nombres de workers, etc.Bien entendu, chaque fois que vous ajoutez un mécanisme de concurrence, vous ajoutez de la complexité, donc ne le faites que si c’est nécessaire.
Commencez avec un programme synchrone simple. Si c’est trop lent, ajouter les threads ou l’asyncio, si ça ne suffit pas, utilisez les deux.
]]>Pour ça, on dégaine pip et installe requests-futures, un plugin pour la célèbre lib requests qui fonctionne avec Python 2 et 3 :
pip install requests-futures
requests-futures va créer pour vous une pool de workers (2 par défaut) et quand vous faites une requête, la lib vous retourne un objet future qui vous permet d’attacher un callback.
Fiou, le nombre de liens référant à d’autres articles du blog est en train d’exploser.
Exemple :
import time
from requests_futures.sessions import FuturesSession
# Cette session est notre point d'entrée, c'est elle
# qui gère nos workers. Faites help(FuturesSession)
# pour voir ses paramètres.
session = FuturesSession()
# Les URLs sur lesquelles on va faire
# nos requêtes
URLs = [
"http://sametmax.com",
"http://sebsauvage.net",
"http://indexerror.net",
"http://afpy.org",
"http://0bin.net"
]
# Notre callback qui sera appelé quand une
# des requêtes sera terminée. Il reçoit
# l'objet future pour seul paramètre
def faire_un_truc_avec_le_resultat(future):
# On est juste intéressé par le résutlat, qui
# est un objet response typique de la lib
# request
response = future.result()
print(response.url, response.status_code)
# On traite chaque URL. Comme on a 2 workers,
# on pourra traiter au mieux 2 URLs en parallèle,
# mais toujours sans bloquer le programme
# principal
for url in URLs:
# On fait notre requête GET
future = session.get(url)
# On rajoute le callback à appeler quand
# le résultat de la requête arrive.
# La flemme de faire la gestion des erreurs.
future.add_done_callback(faire_un_truc_avec_le_resultat)
# Juste pour montrer que c'est bien non bloquant
for x in range(10):
print(x)
time.sleep(1)
Output :
0 1 (u'http://sebsauvage.net/', 200) (u'http://sametmax.com/', 200) 2 (u'http://indexerror.net/', 200) (u'http://0bin.net/', 200) (u'http://www.afpy.org/', 200) 3 4 5 6 7 8 9
On remerciera turgon37 pour sa question sur IndexError qui m’a amené à écrire cet article.
]]>Ok, donc tout ça, ça à l’air de faire la même chose, c’est à dire de faire plusieurs choses en même temps, sans bloquer.
Donc c’est pareil ?
Non. En fait c’est une question de point de vue : non bloquant dans quel contexte ?
Pour rappel, l’IO (Input/Ouput), c’est toute activité qui implique que des données entrent et sortent de votre programme : saisie utilisateur, print sur un terminal, lecture sur une socket, écriture sur le disque, etc. Une opération I/O a plusieurs caractéristiques :
La plupart des programmes bloquent quand ils effectuent une opération I/O. Par exemple, si vous faites ceci en Python :
import urllib2
# télécharge et affiche le contenu de la page d'acceuil de sam et max
print(urllib2.urlopen('http://sametmax.com').read())
print("Coucou")
La ligne print("Coucou")
ne s’exécutera pas tant que la ligne précédente n’aura pas terminé de s’exécuter. Dans ce cas ce n’est pas très grâve, mais dans ce cas là :
import urllib2
mille_urls = obtenir_liste_de_mille_urls()
contenu = []
# télécharge et sauvegarde dans une liste
# le contenu de chacune des 1000 urls
for url in mille_urls:
contenu.append(urllib2.urlopen(url).read())
Chaque url est téléchargée une par une, et comme Internet, c’est vachement lent (300 ms X 1000, ça fait 5 minutes, mine de rien), votre programme va prendre un temps fou. Et pour rien en plus, car votre programme va passer la majeure partie du temps à ne rien faire ! En effet, 99% du temps de votre programme est passé à attendre qu’Internet réponde, pendant que votre CPU se touche les noix.
La programmation asynchrone est une réponse à cela : au lieu d’attendre que se finissent les entrées et les sorties, le programme continue de fonctionner.
Une autre problématique se pose alors : comment obtenir le résultat de l’opération d’I/O, puisqu’on ne sait pas quand il va arriver et qu’on attend pas qu’il arrive ?
C’est là que les systèmes asynchrones font un peu de magie. En vérité, une partie du programme attend, mais discrètement, en arrière plan, au niveau de ce qu’on appelle une boucle d’événements (“events loop”), c’est à dire une boucle infinie qui check régulièrement si une opération I/O ne s’est pas terminée.
Cette boucle est invisible pour vous, votre programme continue de tourner. Mais si une opération I/O envoie des données, alors l’events loop va réagir.
Ca a l’air compliqué, mais en fait, c’est, la plupart du temps, juste une histoire de callback (si la notion vous échappe, je vous renvois à l’article dédié…). Par exemple en Javascript :
var mille_urls = obtenir_liste_de_mille_urls();
var contenu = [];
# notre callback qui va permettre d'ajouter
# le contenu téléchargé à notre liste
var callback = function(data) {
contenu.push(data);
};
# Bon, j'utilise jquery pour simplifier le code...
# On boucle sur les milles URL
$.each(mille_urls, function(index, url) {
# On télécharge le contenu, MAIS comme
# $.get est naturellement non blocante,
# elle ne va pas attendre qu'internet
# réponde pour continuer la boucle, et
# donc on pourra attendre plusieurs réponses
# en même temps. Pour avoir le résultat de
# chaque réponse, on passe un callback qui
# va être appelé quand la réponse arrive.
$.get(url, callback);
});
Comprenez bien la subtilité : à tout moment, il n’y a qu’UN SEUL process javascript qui s’éxécute. Il n’y a pas deux traitements, pas de threads, pas de processus parallèles, rien de tout ça. Simplement, Javascript n’attend pas la réponse de sa requête pour faire la requête suivante, il continu sur sa lancée, et donc peut optimiser les temps d’attente en attendant plusieurs choses en même temps.
Javascript utilise massivement des API asynchrones, c’est au cœur du langage, il n’y a aucun effort à faire pour cela. A l’inverse, Python est synchrone par nature, et il faut vraiment se faire chier pour obtenir un algo asynchrone. Ceci changera avec Python 3.4 qui accueillera tulip dans la stdlib, afin de se moderniser sur ce point. En attendant, si vous voulez faire de l’asynchrone en Python, vous pouvez voir du côté de gevent, monocle ou Tornado. L’alternative est d’utiliser des threads ou des processus séparés, ce qui ne demande rien à installer, mais est un peu verbeux, et est moins performant.
Souvenez-vous que l’I/O, c’est toute entrée et sortie du programme. Un clic sur un bouton, c’est une entrée, mettre à jour un élément du DOM dans le navigateur, c’est une sortie. La programmation asynchrone est donc importante pour la réactivité des programmes.
Par exemple, vous avez 1000 images en haute définition à traiter : il faut les redimensionner, les mettre en noir et blanc et ajouter une ombre sur les bords. Là, la partie de votre programme qui prend le plus de temps, c’est le traitement des images, pas l’I/O, et donc c’est le CPU. Par exemple, en Python :
for image in obtenir_liste_images():
# I/O
data = lire_image(image)
# gros du travail
redimensioner(data)
mettre_en_noir_et_blanc(data)
ajouter_ombre(data)
# I/O
ecrire_image(data, image)
Si vous avez plusieurs ordinateurs, une manière de paralléliser le travail est de mettre 500 images sur l’un, et 500 images sur l’autre, et de lancer le script sur chaque ordi.
Si vous avez plusieurs processeurs dans votre ordi (ce qui est le cas de tous les ordis modernes, et plus seulement les super-calculateurs comme il y a 10 ans), vous pouvez aussi paralléliser le travail sur une seule machine : chaque processeur va s’occuper d’une partie du taf.
Bien entendu, vous pouvez lancer le script 2 fois, mais cela ne marche que sur des travaux simples comme celui là. Et ça suppose que vous connaissez le nombre de CPU que vous voulez faire travailler à l’avance.
Une manière de faire plus propre est d’utiliser des threads ou des processus séparés. En Python, le thread ne servirait à rien, car on se heurterait au GIL, le fameux global interpréteur lock, qui fait qu’une VM n’utilise qu’un processeur, quoi qu’il arrive. Les threads ne sont donc utiles (en Python), que pour l’I/O. Par contre on peut utiliser plusieurs processus :
from multiprocessing import Process
def traiter_les_images(debut, fin):
for image in obtenir_liste_images()[debut, fin]:
# I/O
data = lire_image(image)
# gros du travail
redimensioner(data)
mettre_en_noir_et_blanc(data)
ajouter_ombre(data)
# I/O
ecrire_image(data, image)
# On crée deux processus, un pour traiter les 500 premières images,
# un pour traiter les images de 500 à 1000
p1 = Process(target=traiter_les_images, args=(0, 500))
p2 = Process(target=traiter_les_images, args=(500, 1000))
# On les démarre, ils se séparent alors du programme pour
# devenir indépendant
p1.start()
p2.start()
# on dit au programme d'attendre la fin des deux processus
# CE programme bloque ici, mais les deux processus, eux,
# ne bloquent pas.
p1.join()
p2.join()
Dans cet exemple, il y a TROIS processus : votre programme Python, et les deux processus qui vont traiter les photos, qui consistent ni plus ni moins en la fonction traiter_les_images()
qui a maintenant un process pour elle toute seule.
La plupart des langages ont ce genre de mécanisme pour faire du travail en parallèle. Java utilise les threads par exemple. Javascript utilise les Web Workers.
Nous traitons des données de plus en plus massives (jeux vidéos, encoding divx, retouche d’images, montage de sons…), et maîtriser la parallélisation permet donc d’optimiser les ressources de nos machines modernes afin d’être toujours plus efficace.
Si vous avez un serveur et un client, c’est de la programmation concurrente. Si vous avez un module qui s’occupe des I/O utilisateurs, un qui s’occupe de la base de données et un qui surveille le comportement de l’OS, dans des processus séparés, et qui communiquent entre eux, c’est de la programmation concurrente.
La programmation concurrente suppose que chaque acteur de votre système est indépendant et possède son propre état. Idéalement, les acteurs sont capables de communiquer entre eux. Généralement, ils partagent une ressource à laquelle ils doivent accéder, par exemple un fichier de log. Et c’est là qu’il faut faire attention : certaines ressources ne sont pas faites pour êtres utilisées en même temps par plusieurs process. C’est pour ça qu’on parle d’accès concurrent comme d’un gros problème en informatique.
Un exemple de programmation concurrente en Python serait d’avoir un process qui regarde régulièrement si il y a des mails, et les sauvegarde. Si il reçoit un message suspect, il envoie le message à un autre process, un anti-virus, qui en plus de surveiller l’ordi, peut désinfecter le mail. Exemple :
from multiprocessing import Process, Queue
entree_traiteur_de_mail = Queue()
entree_anti_virus = Queue()
def traiter_les_mails():
# Les processus qui tournent continuellement
# en arrière plan sont juste boucle infinie
while True:
mail = obtenir_mail()
# Si un mail est suspect, on l'envoie
# au processus de l'anti-virus,
# et on attend qu'il nous le renvoie
# tout propres.
# Les deux processus sont indépendant,
# ils fonctionnent l'un sans l'autre et
# ne sont pas dans la même VM.
if mail_est_suspect(mail):
entree_anti_virus.put(mail)
mail = entree_traiteur_de_mail.get()
sauvegarder_mail(mail)
def anti_virus():
while True:
# L'anti-virus vérifie périodiquement
# s'il n'a pas un mail à nettoyer,
# mais n'attend que 0.01 seconde, et si
# rien ne se présente, continue son
# travail.
try:
# Si il y a un mail à désinfecter,
# il le nettoie, et le renvoie
# au processus de traitement de mails.
mail = entree_anti_virus.get(0.01)
desinfecter_mail(mail)
entree_traiteur_de_mail.put(mail)
except TimeoutError:
pass
# L'anti-virus ne fait pas que desinfecter
# les mails, il a d'autres tâches à lui
verifier_virus_sur_system()
# On lance les process. La plupart du temps, il n'y a
# pas de mail suspect, et donc les deux processus
# n'en bloquent pas. En cas de mail suspect ils bloquent
# le temps d'échanger le mail entre eux.
process_traitement_mail = Process(target=traiter_les_mails)
process_anti_virus = Process(target=anti_virus)
process_anti_virus.start()
process_traitement_mail.start()
process_anti_virus.join()
process_traitement_mail.join()
La programmation concurrente est donc une question d’architecture : vous êtes en concurrence ou non si vous décidez de répartir votre code entre plusieurs acteurs indépendant ou non. Les acteurs peuvent avoir des tâches distinctes, et ne pas se bloquer, mais communiquer sur les tâches communes. L’avantage de la programmation concurrente, c’est sa robustesse : si un process plante, le reste de votre programme continue de fonctionner. C’est pour cette raison qu’Erlang, un langage connu pour créer des systèmes increvables, base toute sa philosophie là dessus : un programme Erlang est composé de milliers d’acteurs communiquant entre eux par messages.
Ton exemple de programmation parallèle, c’est aussi une exécution concurrente. Et puis si on fait pleins de processus, pour faire la même tâche d’I/O, ils ne se bloquent pas entre eux, donc c’est non bloquant sur l’I/O, c’est asynchrone !
Allez-vous me dire, fort intelligement. Car nous avons des lecteurs intelligents.
Hé oui, effectivement, ce sont des notions qui se chevauchent. Comme je vous l’ai dit, c’est une question de point de vue. Si on se place du point de vue de l’algo, on peut paralléliser le traitement, ou non. Et il y a plusieurs manières de paralléliser. Si on se place du point de vue de l’I/O, on peut bloquer ou non, et alors on est dans de l’asynchrone. Si on se place du point de vue des acteurs, on peut en avoir plusieurs indépendants ou non, alors on est en concurrence.
En fait, même plusieurs acteurs qui communiquent entre eux sont considérés comme étant chacun en train de faire de l’I/O, avec les autres…
Bref, ces 3 termes, c’est de la sémantiques. Au final, ce qui importe, c’est que vous compreniez les enjeux qu’il y a derrière pour écrire un programme qui fasse son boulot comme il faut, et finisse en temps et en heure.
]]>Heureusement Python vient avec le module multiprocessing, qui permet justement de créer plusieurs processus séparés, et les orchestrer pour qu’ils travaillent ensemble, et ainsi saturer la consommation de ressource de nos serveurs modernes si chers et si puissants.
Prenons un employé de banque que nous appellerons A, et un épagneul Breton, que nous appellerons Catherine.
Euh non…
Prenons plutôt une application qui poll des flux RSS comme Liferea. Liferea a pendant bien longtemps freezé l’intégralité de l’UI pendant la mise à jour de la liste d’articles (ben oui le temps de charger une page Web, la main loop attend). On peut éviter cela en utilisant des threads ou, dans notre, cas, de multiples processus.
Bon, il y a peu de chance que Lifera soit CPU bound, donc c’est vrai que dans ce cas les threads feraient aussi bien, mais c’est pour l’exemple, bande de tatillons.
Pour notre cas de figure, nous avons besoin:
feedparser
, une lib Python qui parse les flux RSS;Pour feedparser avec pip:
pip install feedparser
Ça c’est fait.
Pour le reste, on se fait un petit fichier rssmania.py:
# -*- coding: utf-8 -*-
import time
from time import mktime
from datetime import datetime
from multiprocessing import Process, Queue, TimeoutError
import feedparser
# cette fonction va être utilisée comme worker
# elle va lancer un process qui tourne en boucle et vérifie de manière
# régulière si il y a des flux à mettre à jour
def mettre_a_jour_les_flux(queue_flux_a_mettre_a_jour, queue_de_mises_a_jour_des_flux):
last_update = {}
while True: # une bonne boucle infinie pour la main loop
try:
# on vérifie si il y a un message dans la queue pendant 0.1 secondes
# si oui, on parse le flux (sinon, ça raise une TimeoutError)
flux = queue_flux_a_mettre_a_jour.get(0.1)
feed = feedparser.parse(flux)
nouveaux_articles = []
# pour chaque article, on vérifie si la date de parution est
# antérieur au dernier check, et si oui, on le déclare
# "nouvel article"
for article in feed.entries:
try:
dt = datetime.fromtimestamp(mktime(article.updated_parsed))
if dt > last_update[flux]:
nouveaux_articles.append(article.link)
except KeyError:
nouveaux_articles.append(article.link)
# on balance tous les nouveaux articles dans la queue
if nouveaux_articles:
queue_de_mises_a_jour_des_flux.put((feed.feed.title, nouveaux_articles))
last_update[flux] = datetime.now()
# en cas de time out on repart sur un tour de boucle
# si l'utilisateur fait CTRL+C sur le worker principal, il sera
# broadcasté ici, donc on le catch et on exit proprement
except TimeoutError:
pass
except KeyboardInterrupt:
sys.exit(0)
# worker très basique qui demande la mise à jour de tous les flux
# c'est bourrin, mais c'est pour l'exemple on vous dit !
def demander_la_mise_a_jour_des_flux(queue_de_flux_a_mettre_a_jour, flux_rss):
"""
Demande la mise à jour des flux toutes les 5 minutes
"""
# pareil, petite boucle infinie, temporisation et gestion du CTRL + C
# en gros on ne fait que remplir la queue toutes les 5 minutes
# avec des urls
while True:
try:
for flux in flux_rss:
queue_de_flux_a_mettre_a_jour.put(flux)
time.sleep(300)
except KeyboardInterrupt:
sys.exit(0)
# très important ce if, sinon sous windows le module sera importé plusieurs
# fois et lancera ce bloc plusieurs fois
if __name__ == '__main__':
# les flux à mettre à jour, RAS
flux_rss = (
'http://sametmax.com/feed/',
"http://sebsauvage.net/links/index.php?do=rss",
"http://charlesleifer.com/blog/rss/",
"http://xkcd.com/rss.xml"
)
# les queues. Ces objets sont comme des listes partageables entre
# les workers, sur lesquelles on pourrait faire uniquement insert(0, elem)
# (ici put(elem)) et pop() (ici get()). Des FIFO thread safe quoi.
queue_de_flux_a_mettre_a_jour = Queue()
queue_de_mises_a_jour_des_flux = Queue()
# ici on créé nos workers: on dit quelle fonction lancer avec quels
# arguments. Nos arguments ici sont essentiellement les queues,
# puisque c'est ce qui va nous permettre de partager les infos
# entre les process (qui sont sinon isolés les uns des autres)
worker_qui_met_a_jour_les_flux = Process(target=mettre_a_jour_les_flux,
args=(queue_de_flux_a_mettre_a_jour,
queue_de_mises_a_jour_des_flux))
worker_qui_demande_la_mise_a_jour = Process(target=demander_la_mise_a_jour_des_flux,
args=(queue_de_flux_a_mettre_a_jour,
flux_rss))
# On démarre les workers, et à partir de là, 2 processus sont créés
# et lançant chacun une fonction, les boucles infinies tournent joyeusement
# et une personne est agressée toutes les 7 secondes à New York aussi,
# mais on s'en fout dans notre cas présent.
# Bien faire gaffe que les fonctions soient capables de tourner à vide :-)
worker_qui_met_a_jour_les_flux.start()
worker_qui_demande_la_mise_a_jour.start()
# et voici notre worker principal, qui pop les nouveaux flux tout
# frais, et les affiche à l'écran
try:
while True:
try:
feed, articles = queue_de_mises_a_jour_des_flux.get(0.2)
print "Voici les derniers articles de %s :" % feed
for article in articles:
print "- %s" % article
except TimeoutError:
pass
except KeyboardInterrupt:
pass
finally:
# si la boucle while s'arrête d'une manière ou d'une autre
# on attend que les autres processus s'arrêtent avant de quitter
# En vrai on mettrait beaucoup plus de code que ça, une file
# de controle, peut être un handler de SIGTERM, etc
# là on va à l'essentiel
worker_qui_met_a_jour_les_flux.join()
worker_qui_demande_la_mise_a_jour.join()
print "Fin des haricots"
On lance le bouzin:
python rssmania.py
Python se charge automatiquement de créer 2 subprocess, un qui lance la fonction mettre_a_jour_les_flux()
et un pour demander_la_mise_a_jour_des_flux()
puis il va faire tourner notre bouclinette principale avec amour.
Normalement, au premier lancement ça donne un truc comme ça:
Voici les derniers articles de Sam & Max: Python, Django, Git et du cul : - http://sametmax.com/rassurez-vous-vous-netes-pas-bizarres/ - http://sametmax.com/fonctions-anonymes-en-python-ou-lambda/ - http://sametmax.com/deterer-le-cadavre-dun-troll-non-php-nest-pas-simple/ - http://sametmax.com/concurrence-sans-threads-en-python/ - http://sametmax.com/humour-reflexion-et-cul-la-formule-ne-date-pas-dhier/ - http://sametmax.com/state-machine-en-python-en-labsence-dalgos-recursifs-beneficiant-de-tail-call-optimisation/ - http://sametmax.com/appel-a-contributeurs-impertinents/ - http://sametmax.com/synchroniser-les-freeplugs-les-adaptateurs-reseaux-cpl-de-free/ - http://sametmax.com/incendie-en-espagne-un-megot-peut-se-tranformer-en-arme-mortelle/ - http://sametmax.com/jadore-les-context-managers-python/ Voici les derniers articles de Liens en vrac de sebsauvage : - http://imgur.com/37R4c - http://www.clubic.com/navigateur-internet-mobile/opera-mini/actualite-503834-opera-mini-depasse-200-utilisateurs.html - http://www.lesnumeriques.com/jeux-video/impire-p14041/impire-creez-vos-donjons-comme-a-bonne-epoque-annees-bullfrog-n25461.html - http://sebsauvage.net/links/index.php?Jr5VKg - http://sebsauvage.net/links/index.php?PQUdwA - http://imgur.com/A4xkr
Et 5 minutes plus tard (dans le cas improbable où un article a été publié entre temps), ça affiche les nouveaux articles.
Si vous appuyez sur CTRL + C, SIGINT
va être envoyé à tous les workers, et ils vont tous s’arrêter gentiment. Normalement. En théorie. Souvent ça marche. Sur ma machine.
Je parie que là, maintenant, vous êtes en train de ne pas vous demander “Comment pourrais-je exécuter des actions concurrente sans utiliser de threads en python ?”. Et c’est bien dommage pour vous car la seule chose que j’ai à vous écrire c’est un début de réponse à cette question.
Ouep, les threads c’est pas toujours la joie. Au rayon des inconvénients, on retrouve souvent complexité de conception et de debuggage, librairies externes pas toujours thread safe, dégradation des perfs, aucun contrôle sur la granularité de l’exécution, risques liés aux locks pour toute la partie “Atomicité” etc.
Bon tout n’est quand même pas noir, et dans la majorité des cas, un petit coup de threads sera le plus pratique pour faire ce que vous voulez. Mais si votre besoin est vraiment particulier (ou que vous vous ennuyez beaucoup pendant les vacances) voici une solution assez élégante, qui vous laisse le contrôle absolu (MOUHAHAHA) et qui nécessite souvent peu de modifications de votre code existant.
Pour faire ça, on va utiliser les générateurs que vous connaissez bien.
Quoi de mieux qu’un petit exemple pour commencer :
Comme ils sont tous les trois très cons et qu’ils sont incapables de se mettre d’accord pour savoir qui commence, ils décident de faire ça tous en même temps. Problème, ils veulent tous afficher leur mot selon une temporisation bien précise sans se gêner les uns les autres.
Voici donc la fonction que chacun de nos trois zoophiles veut utiliser. Vous remarquerez que le seul truc qu’elle possède de spécial, c’est le petit yield à la fin de chaque itération. C’est moi qui ai décidé de le rajouter arbitrairement à cet endroit (parce que c’est moi le chef). C’est en effet la seule modification à apporter à la fonction pour la rendre “éclatable”.
def afficher_un_truc_regulierement(truc, delai, nombre):
"""Affiche un "truc" tous les "delais" un certain "nombre" de fois"""
import time
derniere_occur = time.time()
num = 0
while num < nombre:
maintenant = time.time()
if maintenant - derniere_occur > delai:
derniere_occur = maintenant
print str(num) + " : " + truc
num += 1
yield # Je rajoute mon(mes) yield(s) où je veux.
Le placement du yield est important, tous les traitements entre deux yields seront exécutés de façon atomique. Dans le reste de ce tuto, j’appellerai ce groupe de traitements atomique une granule (j’aime bien le mot).
Dès l’ajout du mot-clé yield dans le corps, notre fonction retourne un générateur au lieu de s’exécuter normalement.
On crée ensuite une liste d’actions à effectuer de façon concurrente. Chaque action est un générateur retourné par l’appel à la fonction.
liste_des_actions = []
#Paul :
liste_des_actions.append(afficher_un_truc_regulierement("Loutre", 4, 4))
#Jacques :
liste_des_actions.append(afficher_un_truc_regulierement("Tarentule", 5, 3))
#Le mec bizarre
liste_des_actions.append(afficher_un_truc_regulierement("Musaraigne", 3, 3))
Voici enfin le mécanisme qui permet d’exécuter tout ce beau bordel. Il est assez générique et le code parle de lui même :)
while True: # Boucle infinie
if len(liste_des_actions): # Si il reste des actions
#On itère sur une copie de la liste (avec [:])
#pour pouvoir modifier la liste pendant la boucle
for action in liste_des_actions[:]:
try:
action.next() # On execute une granule
except StopIteration:
#Il n'y a plus de granule dans cette action
#On enlève donc l'action de la liste
liste_des_actions.remove(action)
else:
#Plus aucune action, on finit la boucle infinie
break
print "Tout est bien qui finit bien."
Ici, l’exemple est simpliste mais on peut l’adapter à des fonctions beaucoup plus complexes et nombreuses, qui ne se présentent pas forcement sous forme de boucle.
Je vous laisse avec une piste d’évolution possible qui est assez amusante à implémenter (on rigole avec ce qu’on peut, hein). On peut facilement imaginer un système de priorité dynamique entre les actions. En effet, ici, on ne yield aucune valeur, mais on peut décider d’utiliser le nombre X yieldé (et donc retourné par action.next()) pour sauter les X prochains appels à cette action, ce qui aura pour effet de réduire la priorité de celle-çi par rapport aux autres.
Voilou, j’espère que vous n’utiliserez jamais ça dans du code collaboratif (ou alors si vous n’aimez pas vos collaborateurs à la limite) mais que le jour où vous aurez ce besoin particulier, vous saurez quoi faire.
]]>