Le module Threading

Python propose un module nommé threading, de haut niveau, qui masque la complexité de mise en oeuvre des threads en fournissant:

  • une classe Thread pour exécuter du code dans un nouveau thread;

  • des utilitaires de protection des ressources partagés;

  • une classe Event qui permet aux threads de communiquer entre eux.

Class Thread

Un objet thread peut être instancié sans passage d’argument. Notons tout de même les arguments optionnels suivants:

  • target: définit une fonction qui est appelée par la méthode run();

  • name: donne un nom au thread pour le distinguer de plusieurs threads. La méthode getName() récupère ce nom.

Les principales méthodes de la classe Thread à retenir sont listées ci-dessous.

start()

Start() permet de lancer un nouveau thread et d’y exécuter la méthode run().

run()

Méthode exécutée dans le thread. Si target a été défini, run() l’exécute. Run() peut aussi être surchargée pour contenir directement le code à exécuter. Le thread est alive dès que cette méthode est appelée. Lorsque run() est terminée, soit par la fin de l’exécution, soit par une levée d’exception, le thread est dit dead.

join([timeout])

Attend que le thread se termine. Cette méthode peut être appelée par un autre thread que se met alors en attente de la fin d’exécution du thread principal. Si timeout est fourni, c’est un réel qui détermine en seconde le temps d’attente maximum. Passé ce délai, le thread en attente est débloqué.

isAlive()

Informe sur l’état du thread. Renvoie True si la méthode run() est en cours d’exécution.

Exemples de premiers threads

>>> # -*coding: UTF-8 -*-
>>> from threading import Thread
>>> from time import sleep
>>> from sys import stdout

>>> def visiteur():
>>>     print("Bonjour, c'est Nicolas. Je monte te voir")
>>>     #temps pour Nicolas nécessaire pour monter à l'étage
>>>     sleep(5)
>>>     print("Toc Toc Toc")

>>> if __name__ == '__main__':
>>>     print('Dring')
>>>     sleep(1)
>>>     print 'Oui ? Qui est ce ?'
>>>     threadVisitor = Thread(target=visiteur)
>>>     threadVisitor.start()
>>>     sleep(1)
>>>     print("Ok, dépêche toi alors")

>>>     i=0
>>>     while threadVisitor.isAlive():
>>>         if i==0:
>>>             stdout.write('z'*3)
>>>             i = 1
>>>         else:
>>>             stdout.write('Z'*3)

>>>         stdout.flush()
>>>         sleep(0.4)

>>>     print("Ah, te voilà ! J'ai bien failli attendre !")

Lorsque le code est plus complexe qu’une simple fonction, il peut être judicieux de le regrouper dans une classe dérivée de Thread et de surcharger run(), voire __init__ si nécessaire.

>>> # -*coding: UTF-8 -*-
>>> from threading import Thread
>>> from time import sleep
>>> from sys import stdout
>>> class Ingenieur(Thread):
>>>     def __init__(self, resultats):
>>>         Thread.__init__(self)
>>>         self._resultats = resultats
>>>     def run(self):
>>>         """
>>>         Calcul complexe
>>>         """
>>>         sleep(5)
>>>         self._resultats.extend(['je','ne','sais','pas'])
>>> if __name__ == '__main__':
>>>     resultats = []
>>>     bob = Ingenieur(resultats)
>>>     #lancement de la phase de calcul
>>>     bob.start()
>>>     print('Bob est en train de faire un calcul...')
>>>     fait = False
>>>     i = 0
>>>     while not fait:
>>>         stdout.write('%s ' % str(i))
>>>         stdout.flush()
>>>         i += 1
>>>         bob.join(1)
>>>         if not bob.isAlive():
>>>             fait = True
>>>     print('Voici Bob')
>>>     print('Bob: %s' % ' '.join(resultats))

Excercice

Calculer le carré d’une séquence constituée de N listes d’entiers (obtenues façon aléatoire). Mettre les résultats dans une nouvelle séquence. Deux techniques:

  • Approche séquentielle via une boucle

  • Approche par thread

Vérifier le temps total d’exécution pour chaque approche. Quelle est la plus rapide ?

Class Lock

Lorsque plusieurs threads se partagent des ressources, il est nécessaire de protéger le code par des points de synchronisation. Le module thread fournit des fonctions de création de verrous, encapsulées par deux objets de threading : lock et Rlock

class Lock()

Créé une nouvelle primitive de synchronisation. Deux méthodes sont ensuite accessibles: acquire() et release().

acquire([blocking=1])

Acquiert le verrou et renvoie True en cas de succès. Si blocking est à 1, l’appel de cette méthode bloque le thread si le verrou est déjà locké par un autre thread.Si blocking est à 0, acquire() se contente de renvoyer False pour signaler que le verrou des déjà pris.

release()

Libère le verrou, autorisant d’autres threads à le reprendre. Si plusieurs threads sont en attente de ce verrou, un seul thread est autorisé à l’acquérir. Attention: appeler cette méthode sur un verrou qui n’est pas fermé lève une exception.

Exemple

L’exemple ci-dessous définit une liste globale manipulée par plusieurs instances d’une classe de Thread. La suppression et l’ajout d’éléments dans la liste doit se faire de manière protégée.

>>> # -*coding: UTF-8 -*-
>>> from threading import Thread, Lock
>>> from time import sleep
>>> from sys import stdout
>>>
>>> threads = []
>>> locker = Lock()
>>> liste = ['a','b','c']
>>>
>>> class Manipule(Thread):
>>>
>>>     def __init__(self,name):
>>>         Thread.__init__(self,name=name)
>>>
>>>     def _manip(self):
>>>         """ changer une element de la liste en toute protection"""
>>>         for i in range(5):
>>>             locker.acquire()
>>>             try:
>>>                 liste.remove('a')
>>>                 sleep(0.1)
>>>                 liste.insert(0,'a')
>>>             finally:
>>>                 stdout.write('Modif %i pour %s\n' % (i,self.getName()))
>>>                 locker.release()
>>>
>>>     def run(self):
>>>         threads.append(id(self))
>>>         try:
>>>             self._manip()
>>>         finally:
>>>             threads.remove(id(self))
>>>
>>>
>>> if __name__ == '__main__':
>>>    for i in range(10):
>>>        Manipule('thread %i' % i).start()
>>>
>>>    sleep(0.5)
>>>
>>>    while len(threads) > 0:
>>>        stdout.write('.\n')
>>>        stdout.flush()
>>>        sleep(0.1)
>>>
>>>    stdout.write('\n')

Pour vérifier le dysfonctionnement et l’importance du verrou, il suffit de commenter les instructions locker.release() et locker.acquires(). Erreur garantie lors de la suppression de l’élément !

Class Event

En complément des verrous qui protègent des sections critiques, il existe un mécanisme proposant de coordonner le travail de plusieurs threads: les événements définis par des objets de type Event.

class Event()

Renvoie une instance de type Event, qui peut être considérée comme un drapeau. Cette classe fournit des méthodes pour déterminer l’état du drapeau. Les threads peuvent manipuler ces objets pour se coordonner. L’état interne du drapeau est à False lorsque l’objet est instancié.

isSet()

Renvoie l’état du drapeau

set()

Passe le drapeau à True. Tous les threads en attente de l’événement sont débloqués.

clear()

Passe le drapeau à False. Tous les threads qui attendent l’événement seront bloqués.

wait([timeout])

Permet d’attendre l’événement. Si le drapeau est à True, renvoie la main immédiatement. timeout permet de spécifier un temps en seconde après lequel le thread en attente est débloqué même si l’événement n’a pas eu lieu. Si ce timeout n’est pas précisé, le thread attend indéfiniment.

Excercices et TP

  1. Simuler une course de 4x100 mètres avec passage du relai via « Event »;

  2. Trouver le mot passé en argument d’un script, dans une arborescence donnée, en multi-threading;

File prioritaire

Via le module Queue pour gérer des files Simple / FIFO / LIFO. https://docs.python.org/fr/3/library/queue.html

Exemple d’implémentation : https://pymotw.com/2/Queue/

Excercices et TP

Effectuer un téléchargement en mode « file » (via Queue) du podcast suivant : http://radiofrance-podcast.net/podcast09/rss_20168.xml