INTRODUCTION |
D’après le modèle von Neumann, on peut se représenter un ordinateur comme une machine séquentielle qui, d’après un programme, exécute des instructions les unes après les autres occupant chacune le processeur un certain temps. Dans ce modèle, aucune simultanéité n’est possible et, de ce fait, ni traitement parallèle ni concurrence. Dans la vie de tous jours, les processus parallèles sont pourtant omniprésents : chaque être vivant existe en tant qu’individu à part entière mais de nombreux processus se déroulent simultanément à l’intérieur de son corps. Les avantages du traitement parallèle sont évidents : il permet un gain de performance énorme puisque plusieurs tâches sont résolues en même temps. De plus, la redondance et la chance de survie augmente puisque la panne d’un composant du système n’entraîne pas nécessairement la panne du système dans son ensemble. La parallélisation des algorithmes est toutefois une tâche ardue qui, malgré des efforts de recherche considérables, n’est pas encore parvenue à maturité. Le problème essentiel est que les sous-processus partagent généralement des ressources et dépendent de l’issue d’autres processus. Un processus léger ou fil d’exécution (thread en anglais) est constitué de code s’exécutant en parallèle au sein d’un même programme et un processus est constitué de code qui est exécuté en parallèle hors du programme par le système d’exploitation. Python supporte bien les deux types de parallélismes. Dans cette section, nous n’aborderons cependant uniquement l’utilisation de plusieurs threads au sein d’un même processus, à savoir la programmation multi-thread. |
LE MULTI-THREADING EST PLUS SIMPLE QU’IL N’EN A L’AIR |
En Python, il est très facile d’exécuter le code d’une fonction dans son propre thread. Il suffit pour cela d’importer le module threading et de passer à start_new_thread() le nom de la fonction à exécuter en parallèle ainsi que d’éventuels paramètres qu’il faut empaqueter dans un tuple. Le thread débute alors immédiatement son exécution avec le code de la fonction mentionnée..
from gturtle import * import thread def paint(t, isLeft): for i in range(16): t.forward(20) if isLeft: t.left(90) else: t.right(90) t.forward(20) if isLeft: t.right(90) else: t.left(90) tf = TurtleFrame() john = Turtle(tf) john.setPos(-160, -160) laura = Turtle(tf) laura.setColor("red") laura.setPenColor("red") laura.setPos(160, -160) thread.start_new_thread(paint, (john, False)) thread.start_new_thread(paint, (laura, True))
|
MEMENTO |
Pour mouvoir les deux tortues dans la même fenêtre, il faut utiliser explicitement un objet TurtleFrame tf et le passer en argument au constructeur des tortues. Un nouveau thread est créé et démarré immédiatement avec start_new_thread(). Le thread est terminé dès que l’exécution de la fonction avec laquelle il a été lancé parvient à son terme. La liste de paramètres doit être spécifiée dans un tuple. Notez qu’il faut de ce fait passer un tuple vide () pour démarrer une fonction qui ne prend aucun paramètre. Attention encore à la subtilité suivante pour un tuple ne comportant qu’un seul élément x : il n’est pas représenté par (x) mais par (x, ). |
CRÉER ET DÉMARRER DES THREADS EN TANT QU’INSTANCE D’UNE CLASSE |
from threading import Thread from random import random from gturtle import * class TurtleAnimator(Thread): def __init__(self, turtle): Thread.__init__(self) self.t = turtle def run(self): while True: self.t.forward(150 * random()) self.t.left(-180 + 360 * random()) tf = TurtleFrame() john = Turtle(tf) john.wrap() laura = Turtle(tf) laura.setColor("red") laura.setPenColor("red") laura.wrap() thread1 = TurtleAnimator(john) thread2 = TurtleAnimator(laura) thread1.start() thread2.start()
|
MEMENTO |
Même dans un système multiprocesseur, le code n'est pas vraiment exécuté en parallèle mais plutôt par tranches temporelles successives. De ce fait, le traitement des données demeure généralement "quasi" parallèle. Il est toutefois important de comprendre que l’allocation du temps processeur aux différents threads a lieu à des instants non prévisibles, à savoir n’importe où au milieu de l’exécution d’un bout de code. Lorsque le processeur passe à l’exécution d’un autre thread, le point d’interruption du code du thread actuel ainsi que l’état des variables locales sont bien entendu sauvegardés et restaurés lors de la reprise de l’exécution du thread. Il n’en demeure pas moins que des problèmes épineux peuvent survenir lorsque d’autres threads modifient entre temps des variables globales, ce qui s’applique également au contenu d’une fenêtre graphique. Il ne va donc pas de soi qu’aucun problème ne survienne lorsque l’on a deux tortues dans deux threads différents. [plus... Pour éviter de tels problèmes, la bibliothèque de tortues dut être développée spécifiquement pour éviter ces problèmes : on dit qu’elle est thread-safe.]. Comme vous pouvez le constater, la partie principale du programme se termine mais les deux threads continuent d’effectuer leur tâche jusqu’à la fermeture de la fenêtre. |
TERMINER LES THREADS |
Une fois démarré, un thread ne peut pas être terminé directement depuis une méthode extérieure à lui-même, comme par exemple depuis un autre thread. Pour que le thread se termine, il faut nécessairement que l’exécution de sa méthode run() se termine. C’est la raison pour laquelle il n’est jamais une bonne idée de mettre une boucle while infinie dans la méthode run() d’un thread. Il faut plutôt utiliser une variable globale booléenne isRunning pour contrôler l’exécution de la boucle while. Cette variable sera normalement mise à True tant que le thread doit s’exécuter mais peut également être mise sur False depuis un autre thread pour demander son arrêt. Dans le programme suivant, les deux tortues effectuent un mouvement aléatoire jusqu’à ce que l’une des deux sorte de la surface circulaire. from threading import Thread from random import random import time from gturtle import * class TurtleAnimator(Thread): def __init__(self, turtle): Thread.__init__(self) self.t = turtle def run(self): while isRunning: self.t.forward(50 * random()) self.t.left(-180 + 360 * random()) tf = TurtleFrame() john = Turtle(tf) laura = Turtle(tf) laura.setColor("red") laura.setPenColor("red") laura.setPos(-200, 0) laura.rightCircle(200) laura.setPos(0, 0) thread1 = TurtleAnimator(john) thread2 = TurtleAnimator(laura) isRunning = True thread1.start() thread2.start() while isRunning and not tf.isDisposed(): if laura.distance(0, 0) > 200 or john.distance(0, 0) > 200: isRunning = False time.sleep(0.001) tf.setTitle("Limit exceeded")
|
MEMENTO |
Il ne faudrait jamais utiliser une boucle « serrée » qui n’effectue aucune action car cela gaspille beaucoup de temps processeur pour rien. Il faut toujours aérer la boucle avec un court temps d’attente avant de boucler en recourant à time.sleep(), Turtle.sleep() ou GPanel.delay(). Une fois qu’un thread s’est terminé, il n’est plus possible de le redémarrer. Un nouvel appel à la méthode start() produit en effet un message d’erreur. |
METTRE DES THREADS EN PAUSE |
from threading import Thread from random import random import time from gturtle import * class TurtleAnimator(Thread): def __init__(self, turtle): Thread.__init__(self) self.t = turtle def run(self): while True: if isPaused: Turtle.sleep(10) else: self.t.forward(100 * random()) self.t.left(-180 + 360 * random()) tf = TurtleFrame() john = Turtle(tf) laura = Turtle(tf) laura.setColor("red") laura.setPenColor("red") laura.setPos(-200, 0) laura.rightCircle(200) laura.setPos(0, 0) thread1 = TurtleAnimator(john) thread2 = TurtleAnimator(laura) isPaused = False thread1.start() thread2.start() tf.setTitle("Running") while not isPaused and not tf.isDisposed(): if laura.distance(0, 0) > 200 or john.distance(0, 0) > 200: isPaused = True tf.setTitle("Paused") Turtle.sleep(2000) laura.home() john.home() isPaused = False tf.setTitle("Running") time.sleep(0.001) Il est même bien plus avisé de stopper un thread avec Monitor.putSleep() et de reprendre son exécution par la suite avec Monitor.wakeUp(). . from threading import Thread from random import random import time from gturtle import * class TurtleAnimator(Thread): def __init__(self, turtle): Thread.__init__(self) self.t = turtle def run(self): while True: if isPaused: Monitor.putSleep() self.t.forward(100 * random()) self.t.left(-180 + 360 * random()) tf = TurtleFrame() john = Turtle(tf) laura = Turtle(tf) laura.setColor("red") laura.setPenColor("red") laura.setPos(-200, 0) laura.rightCircle(200) laura.setPos(0, 0) thread1 = TurtleAnimator(john) thread2 = TurtleAnimator(laura) isPaused = False thread1.start() thread2.start() tf.setTitle("Running") while not isPaused and not tf.isDisposed(): if laura.distance(0, 0) > 200 or john.distance(0, 0) > 200: isPaused = True tf.setTitle("Paused") Turtle.sleep(2000) laura.home() john.home() isPaused = False Monitor.wakeUp() tf.setTitle("Running") time.sleep(0.001)
|
MEMENTO |
Un thread peut se mettre lui-même en pause avec la méthode bloquante Monitor.putSleep() de sorte qu’il ne gaspille aucun temps processeur. Un autre thread peut alors le réactiver avec la méthode Monitor.wakeUp() qui va obliger la méthode Monitor.putSleep() à retourner. |
ATTENDRE LE RÉSULTAT DU TRAITEMENT D’UN THREAD |
Dans ce programme, on utilise un thread ouvrier (worker thread) pour calculer la somme des nombres entiers entre 1 et 1'000'000. Le programme principal attend le résultat et affiche le temps nécessaire pour l’effectuer. Comme ce temps peut légèrement varier, on refait la mesure 10 fois de suite dans un thread ouvrier. On utilise join() pour attendre la fin de l’exécution du thread. from threading import Thread import time class WorkerThread(Thread): def __init__(self, begin, end): Thread.__init__(self) self.begin = begin self.end = end self.total = 0 def run(self): for i in range(self.begin, self.end): self.total += i startTime = time.clock() repeat 10: thread = WorkerThread(0, 1000000) thread.start() thread.join() print(thread.total) print("Time elapsed:", time.clock() - startTime, "s") Comme dans la vie réelle, on peut déléguer du travail pénible à plusieurs ouvriers différents. Si l’on utilise deux threads ouvriers pour que chacun effectue la moitié du travail, il faudra attendre que les deux aient terminé pour combiner leur résultat. from threading import Thread import time class WorkerThread(Thread): def __init__(self, begin, end): Thread.__init__(self) self.begin = begin self.end = end self.total = 0 def run(self): for i in range(self.begin, self.end): self.total += i startTime = time.clock() repeat 10: thread1 = WorkerThread(0, 500000) thread2 = WorkerThread(500000, 1000000) thread1.start() thread2.start() thread1.join() thread2.join() result = thread1.total + thread2.total print result print "Time elapsed:", time.clock() - startTime, "s"
|
MEMENTO |
On pourrait également mettre un fanion global isFinished() à True lorsque le thread se termine et tester ce fanion dans une boucle while dans la partie principale du programme. Cette solution est cependant moins élégante que l’utilisation de join() qui présente l’avantage de ne pas gaspiller inutilement des cycles processeur en testant sans arrêt le fanion. |
SECTIONS CRITIQUES ET VERROUS |
Puisque les threads s’exécutent de manière indépendante, la modification de données communes par plusieurs threads est délicate. Pour éviter des collisions non souhaitées entre les threads, on regroupe les instructions qui doivent nécessairement être exécutées sans interruption en les regroupant dans un bloc de programme appelée section critique muni de protections garantissant qu’il sera exécuté sans interruption, de manière atomique. Si un autre thread tente d’exécuter ce bloc de code, il doit attendre jusqu’à ce que le thread actuel ait terminé l’exécution de ce même bloc de code. Cette protection est implémentée dans les programmes Python à l’aide d’un verrou (lock en anglais). Un verrou est une instance de la classe Lock qui possède deux états possibles : verrouillé (locked) et déverrouillé (unlocked) ainsi que deux méthodes, acquire() et release() fonctionnant selon les règles suivantes :
On dit qu’un thread acquière le verrou avec acquire() et le libère à nouveau avec release() [plus...
Dans d’autres langages de programmation, le mécanisme de verrou est Voici plus précisément comment procéder pour protéger une section critique : on commence par créer un objet global lock = Lock() qui est bien entendu dans un état initial déverrouillé. Chaque thread tente ensuite d’acquérir le verrou avec acquire() en entrant dans la section critique. En cas d’échec, si le verrou est déjà verrouillé par un autre thread, le thread demandeur est automatiquement mis en veille jusqu’à la libération du verrou. Lorsqu’un thread acquière le verrou, il traverse la section critique et libère le verrou lorsqu’il a terminé avec release() de sorte que les autres threads puissent à leur tour l’acquérir [plus...
Lorsque plusieurs threads sont en attente du verrou, c’est le système d’exploitation
Dans le programme suivant, le dessin et la suppression de carrés remplis constitue la section critique. On efface un carré en repeignant par-dessus avec la couleur d’arrière-fond blanche. Le thread principal crée un carré rouge clignotant en dessinant le carré rouge et en l’effaçant après un court laps de temps. Dans un second thread MyThread, le clavier est sans arrêt interrogé avec getKeyCode(). Si l’utilisateur presse sur la barre d’espace, le carré est déplacé à une position aléatoire. Il est évident que la section critique doit être protégée par un verrou. Si le déplacement du carré avait lieu pendant qu’il en train d’être dessiné ou effacé, le comportement serait chaotique. from gpanel import * from threading import Thread, Lock from random import randint class MyThread(Thread): def run(self): while not isDisposed(): if getKeyCode() == 32: print("----------- Lock requested by MyThread") lock.acquire() print("----------- Lock acquired by MyThread") move(randint(2, 8), randint(2, 8)) delay(500) # for demonstration purposes print("----------- Lock releasing by MyThread...") lock.release() else: delay(1) def square(): print("Lock requested by main") lock.acquire() print("Lock acquired by main") setColor("red") fillRectangle(2, 2) delay(1000) setColor("white") fillRectangle(2, 2) delay(1000) print("Lock releasing by main...") lock.release() lock = Lock() makeGPanel(0, 10, 0, 10) t = MyThread() t.start() move(5, 5) while not isDisposed(): square() delay(1) # Give up thread for a short while |
MEMENTO |
On peut observer dans la console la manière dont chaque thread attend sagement son tour jusqu’à ce que le verrou soit libéré : Lock requested by main Lock acquired by main ----------- Lock requested by MyThread Lock releasing by main... ----------- Lock acquired by MyThread Lock requested by main ----------- Lock releasing by MyThread... Lock acquired by main Essayez de désactiver le mécanisme du verrou en commentant les lignes où il est acquis et libéré. Vous verrez que le carré n’est plus dessiné et effacé correctement. Rappelez-vous également qu’il faut toujours ajouter un petit temps d’attente dans la boucle while pour éviter de consommer trop de cycles processeur inutilement. |
GUI-WORKERS |
Les fonctions de rappel lancées par un composant GUI (GUI = Graphical User Interface) s’exécutent dans un thread propre au système nommé Event Dispatch Thread (EDT). Celui-ci est responsable de garantir un rendu correct de la fenêtre graphique ainsi que de ses différents composants (boutons, etc.). Du fait que le rendu s’effectue à la fin de la fonction de rappel, l’interface graphique est gelée jusqu’à ce que cette dernière se termine. Il n’est de ce fait pas possible de programmer des animations graphiques au sein des fonctions de rappel d’interface graphique. Il faut impérativement suivre la règle suivante de manière très stricte :
En l’occurrence, une tâche est considérée de longue durée si son exécution excède une durée de 10 ms. Il faut estimer cette durée d’exécution en tenant compte des pires conditions possibles telles qu’un processeur lent et une charge système élevée. Si une action dure trop longtemps, il faut l’exécuter dans un thread séparé appelé GUI worker en anglais. Le programme suivant dessine une rosace lors d’un clic sur l’un des deux boutons. Le dessin est animé et prend un certain temps. Il faut de ce fait effectuer le dessin dans un thread ouvrier, ce qui ne devrait vous poser aucun problème vu vos connaissances sur les threads. Ce fonctionnement soulève cependant un autre problème : puisque chaque clic sur un bouton va lancer un nouveau thread de rendu, plusieurs dessins peuvent être démarrés simultanément, ce qui va rapidement conduire au chaos le plus absolu. On peut éviter cette situation très simplement en désactivant les boutons durant l’exécution du dessin [plus... ce qui constitue un comportement d’interface utilisateur tout-à-fait légitime contrairement au gel de l’ensemble de l’interface].
from gpanel import * from javax.swing import * import math import thread def rho(phi): return math.sin(n * phi) def onButtonClick(e): global n enableGui(False) if e.getSource() == btn1: n = math.e elif e.getSource() == btn2: n = math.pi # drawRhodonea() thread.start_new_thread(drawRhodonea, ()) def drawRhodonea(): clear() phi = 0 while phi < nbTurns * math.pi: r = rho(phi) x = r * math.cos(phi) y = r * math.sin(phi) if phi == 0: move(x, y) else: draw(x, y) phi += dphi enableGui(True) def enableGui(enable): btn1.setEnabled(enable) btn2.setEnabled(enable) dphi = 0.01 nbTurns = 100 makeGPanel(-1.2, 1.2, -1.2, 1.2) btn1 = JButton("Go (e)", actionListener = onButtonClick) btn2 = JButton("Go (pi)", actionListener = onButtonClick) addComponent(btn1) addComponent(btn2) validate() |
MEMENTO |
Seul un code de très courte durée (< 10 ms) peut être placé dans les fonctions de rappel lancées par les composants GUI. Placer un code prenant plus de temps dans un gestionnaire d’événement de l’interface graphique mène au gel de l’interface utilisateur, ce qui est très désagréable. Il faut donc déléguer les traitements trop longs (> 10 ms) à un thread ouvrier séparé. Dans une interface graphique, seuls les boutons ou menus menant à des actions permises et utiles devraient être actifs. Les composants qui lancent des opérations interdites ou insensées devraient être désactivés. |
MATÉRIEL SUPPLÉMENTAIRE |
SITUATIONS DE COMPÉTITION, INTERBLOCAGE |
Les êtres humains travaillent de manière hautement parallèle mais leur raisonnement logique est essentiellement séquentiel. Il nous est de ce fait très difficile de conserver une vue d’ensemble de l’exécution de programmes multi threads. Voilà pourquoi il faut toujours utiliser les threads avec une grande précaution, aussi élégants puissent-ils paraître de prime abord. Hormis dans les applications basées sur des données et traitements aléatoires, un programme devrait toujours retourner le même résultat (postcondition) lorsqu’on lui fournit les mêmes conditions initiales (préconditions). Ceci n’est en aucun cas garanti pour les programmes comportant plusieurs threads accédant à des données partagées, même si les sections critiques sont protégées par des verrous. Le programme suivant démontre cette affirmation : il comporte deux threads, thread1 et thread2, effectuant une addition et une multiplication de deux nombres globaux a and b. Les variables globales a et b sont protégées par les verrous lock_a et lock_b. La programme principal initialise et démarre les deux threads et attend ensuite qu’ils se terminent. La valeur finale des variables a et b est finalement affichée dans la console. Les threads sont ici générés de manière différente en spécifiant la méthode run() en tant que paramètre nommé du constructeur de la classe Thread. from threading import Thread, Lock from time import sleep def run1(): global a, b print("----------- lock_a requested by thread1") lock_a.acquire() print("----------- lock_a acquired by thread1") a += 5 # sleep(1) print("----------- lock_b requested by thread1") lock_b.acquire() print("----------- lock_b acquired by thread1") b += 7 print("----------- lock_a releasing by thread1") lock_a.release() print("----------- lock_b releasing by thread1") lock_b.release() def run2(): global a, b print("lock_b requested by thread2") lock_b.acquire() print("lock_b acquired by thread2") b *= 3 # sleep(1) print("lock_a requested by thread2") lock_a.acquire() print("lock_a acquired by thread2") a *= 2 print("lock_b releasing by thread2") lock_b.release() print("lock_a releasing by thread2") lock_a.release() a = 100 b = 200 lock_a = Lock() lock_b = Lock() thread1 = Thread(target = run1) thread1.start() thread2 = Thread(target = run2) thread2.start() thread1.join() thread2.join() print("Result: a =", a, ", b =", b) |
MEMENTO |
En exécutant ce programme suffisamment de fois, on peut observer que le résultat affiché est tantôt a = 205, b = 607, tantôt a = 210, b = 621. Il arrive même parfois que l’exécution du programme ne se termine pas ! Voici l’explication de ce phénomène : Bien que thread1 soit créé en premier et démarré depuis le programme principal avant thread2, on ne peut pas garantir qu’il soit le premier à pénétrer dans la section critique puisque la première ligne est parfois lock_a requested by thread1 et parfois lock_b requested by thread2 Le cours des événements n’est ensuite pas non plus uniquement déterminé puisque le basculement entre les threads peut survenir à n’importe quel endroit du code. Il est tout-à-fait possible que la multiplication des nombres a et b soit effectuée en premier, ce qui explique les résultats divergents. Puisque les deux threads s’exécutent (run en anglais) ensemble comme dans une compétition, on parle dans ce cas de situation de compétition (race condition en anglais).. La situation peut d’ailleurs prendre une tournure encore plus fâcheuse et geler complètement le programme. Juste avant le blocage du programme, on peut alors observer les lignes suivantes dans la console : ----------- lock_a requested by thread1 lock_b requested by thread2 lock_b acquired by thread2 lock_a requested by thread2 ----------- lock_a acquired by thread1 ----------- lock_b requested by thread1 Il faut un sacré travail de détective pour comprendre ce qui s’est passé dans ce cas. Tentons tout de même le coup : apparemment, le thread1 commence l’exécution en premier et tente d’acquérir le verrou lock_a. Avant qu’il n’ait l’occasion de terminer l’exécution de cette section critique, le thread2 tente d’acquérir le verrou lock_b avec succès. Immédiatement, le thread2 tente également d’acquérir le verrour lock_a, mais en vain puisque celui-ci a déjà été acquis par le thread1. De ce fait, le thread2 est bloqué en attendant la libération du verrou par thread1 qui continue son exécution et tente d’acquérir le verrou lock_b. C’est là que les choses se gâtent car le verrou lock_b est déjà acquis par le thread2 qui est bloqué dans son exécution. Ainsi, les deux threads se bloquent mutuellement sans espoir de pouvoir libérer le verrou nécessaire à l’autre thread pour être débloqué, ce qui bloque tout le programme. On appelle cela une situation d’interblocage (deadlock en anglais). L’activation des instructions sleep(1) par suppression du commentaire permet de conduire le programme systématiquement dans cette condition. Réfléchissez à cela pour essayer de bien comprendre ce phénomène. Comme vous pouvez le constater, un interblocage survient lorsque deux threads thread1 et thread2 dépendent de deux ressources partagées a et b et se bloquent mutuellement. Par conséquent, il peut arriver que le thread2 attende sur le verrou lock_a et que le thread1 attende sur le verrou lock_b , se bloquant mutuellement de telle sorte que les verrous n’ont aucune chance d’être libérés. Pour éviter les situations d’interblocage, il faut scrupuleusement adhérer à la règle suivante :
|
EXPRESSIONS ATOMIQUES ET THREAD-SAFE |
Si plusieurs threads sont impliqués dans un programme, on ne connait jamais, en tant que programmeur, à quel moment et à quel endroit du code va avoir lieu le basculement entre threads. Comme nous l’avons déjà constaté, cela peut conduire à des résultats inattendus et incorrects lorsque les threads travaillent avec des ressources partagées. Ceci est particulièrement vrai si plusieurs threads changent une fenêtre de sorte que, lorsqu’un thread ouvrier est généré au sein d’une fonction de rappel GUI pour exécuter un code de longue durée, il faut presque toujours s’attendre à des situations chaotiques. Dans le programme précédent, nous avons évité ce problème en désactivant les boutons durant l’exécution des fonctions de rappel. On peut s’assurer que plusieurs threads puissent s’exécuter en parallèle sans se marcher sur les pieds en prenant des précautions particulières. Un tel code est alors appelé thread-safe. Écrire du code thread-safe pour qu’il puisse être exécuté dans en environnement multi-threads sans conflit est un véritable art. [plus.. On appelle réentrance la situation qui survient lorsqu’un thread exécute une fonction et est interrompu par un autre thread qui appelle la même fonction. Une fonction thread-safe peut sans problème être appelée de manière réentrante].
from gturtle import * import thread def onMousePressed(event): # createStar(event) thread.start_new_thread(createStar, (event,)) def createStar(event): t = Turtle(tf) x = t.toTurtleX(event.getX()) y = t.toTurtleY(event.getY()) t.setPos(x, y) t.startPath() repeat 9: t.forward(100) t.right(160) t.fillPath() tf = TurtleFrame(mousePressed = onMousePressed) tf.setTitle("Klick To Create A Working Turtle") |
MEMENTO |
Si l’on ne génère pas un nouveau thread (décommenter la ligne commentée et commenter l’autre) on n’observe les étoiles que lorsqu’elles sont terminées. Il est cependant possible d’écrire le programme sans qu’il n’utilise son propre thread séparé en utilisant le paramètre nommé mouseHit au lieu de mousePressed comme nous l’avions fait au chapitre chapitre 2.11. Dans ce cas, le thread est alors automatiquement engendré par la bibliothèque de graphiques tortues. Il est important de savoir que le basculement entre threads peut même survenir en plein milieu d’une ligne de code. Un changement de thread peut par exemple survenir au beau milieu de la ligne a = a + 1 ou même a += 1 entre le moment où la variable est lue et celui où la valeur y est écrite. Par contraste, une expression est appelée atomique si elle ne peut pas être interrompue par un changement de thread. Comme pour la plupart des langages de programmation, Python n’est pas vraiment atomique. Il peut arriver qu’une instruction print soit interrompue par des prints survenant dans d’autres threads, ce qui peut résulter en une sortie complètement chaotique vers la console. C’est au programmeur qu’incombe la lourde tâche de rendre atomiques et thread-safe les fonctions, les expressions et les sections du code en recourant aux verrous. |
EXERCICES |
|