thread – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 En Python, les threads et l’asyncio s’utilisent ensemble http://sametmax.com/en-python-les-threads-et-lasyncio-sutilisent-ensemble/ http://sametmax.com/en-python-les-threads-et-lasyncio-sutilisent-ensemble/#comments Wed, 13 Jan 2016 12:11:12 +0000 http://sametmax.com/?p=17796 En Python, les threads et l’asyncio s’utilisent ensemble

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.

Faire des requêtes pendant que ça bloque

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.

Asyncio a de l’overhead

On a vendu asyncio comme plus léger que les threads. Ce n’est pas tout à fait exact. asyncio a les avantages suivants :

  • Pas besoin de préallouer des threads. Ca scale toujours pile poil, et ça bouffe moins de mémoire.
  • asyncio est plus performant quand la charge de travail demande de très nombreux threads.
  • un algo asyncio est plus facile à conceptualiser.
  • il est plus difficile d’introduire des erreurs de concurrence.
  • asyncio est plus facile à débugger.

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.

Quoi faire ?

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 :

  • Faites des tests pour voir si vous y gagner vraiment.
  • Ecrivez un petit wrapper sur tout ça car là c’est verbeux.
  • Vous pouvez passer un 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.

]]>
http://sametmax.com/en-python-les-threads-et-lasyncio-sutilisent-ensemble/feed/ 8 17796
Utiliser requests de manière non bloquante facilement http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/ http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/#comments Wed, 21 Jan 2015 07:17:41 +0000 http://sametmax.com/?p=15797 En attendant le dossier sur la programmation non bloquante, voici une petite lib qui résout un cas d’école : faire une requête HTTP sans bloquer avec une jolie API, en pur Python.

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.

]]>
http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/feed/ 12 15797
La différence entre la programmation asynchrone, parallèle et concurrente http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/ http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/#comments Wed, 09 Oct 2013 22:08:13 +0000 http://sametmax.com/?p=7378 On parle un peu partout de programmation non bloquante ces temps-ci. NoSQL a remis le map/reduce au goût du jour, et PAF, on vous sort le mot clé parallélisation pour vous en vendre une tetrachiée. Les partisants de NodeJS vont crier “asynchrone”, parce que c’est ce que Javascript sait faire de mieux. Et on murmure dans les coins que la robustesse d’Erlang tient dans ses acteurs qui travaillent de manière concurrente dans la VM.

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 ?

Si c’est l’IO, c’est asynchrone

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 :

  • Le temps que prend l’opération n’est pas dépendant du CPU : la vitesse du disque, la latence du réseau, le nombre d’heures de sommeil du sysadmin sont les facteurs qui vont déterminer quand l’opération va prendre fin.
  • Le corollaire, c’est qu’on ne peut pas prédire quand l’opération va prendre fin depuis le programme.
  • Sur les services avec beaucoup d’I/O (serveurs Web, bases de données, crawlers, scripts de déploiement, etc), c’est l’I/O qui généralement prend le plus de temps dans l’exécution du programme. L’optimisation de ces opérations va donc l’accélérer bien plus que de changer votre algo.

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.

Si un algorithme peut répartir son travail en plusieurs bouts, c’est parallèle

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 il y a plusieurs entités indépendantes, c’est concurrent

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.

Hey, mais, attends là !

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.

]]>
http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/feed/ 35 7378
Remplacer les threads avec le module multiprocessing en Python http://sametmax.com/remplacer-les-threads-avec-le-module-multiprocessing-en-python/ http://sametmax.com/remplacer-les-threads-avec-le-module-multiprocessing-en-python/#comments Tue, 31 Jul 2012 12:24:03 +0000 http://sametmax.com/?p=1430 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.]]> Les threads Python sont limités par le Global Interpreter Lock, et si ils permettent de s’affranchir des problèmes de concurrence d’accès IO, ils sont inefficaces pour profiter de nos merveilleux processeurs multi-coeurs. Les coroutines, une alternative élégante aux threads, ont la même limitation.

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:

  • d’un process qui demande aux autres de vérifier les derniers flux RSS (pour simuler une interaction utilisateur);
  • d’un process qui va faire la vérification des flux RSS sans bloquer les autres process;
  • de feedparser, une lib Python qui parse les flux RSS;
  • d’un process qui va lancer tout ça, récupérer le résultat et l’afficher.

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.

]]>
http://sametmax.com/remplacer-les-threads-avec-le-module-multiprocessing-en-python/feed/ 15 1430
Concurrence sans threads en python http://sametmax.com/concurrence-sans-threads-en-python/ http://sametmax.com/concurrence-sans-threads-en-python/#comments Sat, 28 Jul 2012 00:51:37 +0000 http://sametmax.com/?p=1373 Ceci est un post invité de poulpe posté sous licence creative common 3.0 unported.

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.

Pourquoi faire ?

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.

Comment faire ?

Pour faire ça, on va utiliser les générateurs que vous connaissez bien.
Quoi de mieux qu’un petit exemple pour commencer :

  • Paul veut afficher de façon régulière le mot “Loutre”.
  • Jacques, lui, voudrait afficher de la même façon le mot “Tarentule”.
  • Un mec bizarre que personne ne connait désire écrire “Musaraigne”.

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.

Comment faire mieux ?

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.

]]>
http://sametmax.com/concurrence-sans-threads-en-python/feed/ 10 1373