Concurrence sans threads en python


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.

10 thoughts on “Concurrence sans threads en python

  • Max

    Félicitations à poulpe pour son premier article for interessant sur S&M.
    Longue vie au partage et à poil les putes bien sûr !!!

  • Sam

    A noter que ce principe est un usage de co-routine, on créé ici des pseudo-thread: la concurrence est simulée en s’affranchissant surtout des limites IO.

    Contrairement au thread, on a pas de problème de synchro, et comme les threads sont de toute façon limités par le GIL et qu’ils ne servent vraiment que pour les IO blocking, c’est tout bénéf en Python.

    Si vous n’avez pas envie de vous coder tout le tout le brouzouf à la main, les greenlets appliquent très bien ce principe, et on trouve des algo tout fait pour la plupart des uses cases comme avec la lib gevent:

    http://www.gevent.org/

    Si vous souhaitez avoir de la concurrence CPU, les coroutines (ni les threads d’ailleurs) ne vous aideront. Dans ce cas, le module subprocess est votre ami (ou une bonne queue avec des libs genre celery).

  • Le type bizarre que personne ne connaît

    Musaraigne
    Musaraigne

    Muuuusaaaaraaaiigne !!!!!!

    (Désolé pour le post inutile, pas pu m’en empêcher)

    Au reste, chouette article.

    • Sam

      Ton adresse email est géniale. Mais le pire, c’est que le nom de domaine existe !

      Par contre tu vas en chier à retaper ce pseudo à chaque fois.

  • Luigi

    Autocomplete du pseudo et email pour les commentaires ;o) Il ne devrait pas trop avoir besoin de se fatiguer.

    Cet article vole trop haut pour moi (ou je n’en ai pas l’utilité). Mais je range ça si jamais…

    J’en profite également pour remercier tous ceux qui suivent et contribuent au site. Beaucoup d’articles intéressants (et j’aime beaucoup la forme de mini-tutos). Continuez comme ça.

    • Max

      Merci mec!

      Bientôt un tuto sur comment pêcher la baleine à poils pubiens orange du Nebraska avec un cure-dent et une pince à linge.

      stay tuned!

  • YCL1

    Musaraigne !

    En dépoussiérant cet article je viens de voir que le premier code contient une erreur html (<)

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.