.. nperrier


########################
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


