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. 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 ) ) 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." 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.