Programmation Python/Threads
Gestion de plusieurs tâches en parallèle à l'aide des threads.
Le système de communication que nous avons élaboré dans les pages précédentes est vraiment très rudimentaire : d'une part il ne met en relation que deux machines seulement, et d'autre part il limite la liberté d'expression des deux interlocuteurs. Ceux-ci ne peuvent en effet envoyer des messages que chacun à leur tour. Par exemple, lorsque l'un d'eux vient d'émettre un message, son système reste bloqué tant que son partenaire ne lui a pas envoyé une réponse. Lorsqu'il vient de recevoir une telle réponse, son système reste incapable d'en réceptionner une autre, tant qu'il n'a pas entré lui-même un nouveau message, ... et ainsi de suite.
Tous ces problèmes proviennent du fait que nos scripts habituels ne peuvent s'occuper que d'une seule chose à la fois. Lorsque le flux d'instructions rencontre une fonction codice_1, par exemple, il ne se passe plus rien tant que l'utilisateur n'a pas introduit la donnée attendue. Et même si cette attente dure très longtemps, il n'est habituellement pas possible que le programme effectue d'autres tâches pendant ce temps. Ceci n'est toutefois vrai qu'au sein d'un seul et même programme : vous savez certainement que vous pouvez exécuter d'autres applications entre-temps sur votre ordinateur, car les systèmes d'exploitation modernes sont "multi-tâches".
Les pages qui suivent sont destinées à vous expliquer comment vous pouvez introduire cette fonctionnalité multi-tâches dans vos programmes, afin que vous puissiez développer de véritables applications réseau, capables de communiquer simultanément avec plusieurs partenaires.
Veuillez à présent considérer le script de la page précédente. Sa fonctionnalité essentielle réside dans la boucle codice_2 des lignes 23 à 29. Or, cette boucle s'interrompt à deux endroits :
Ces deux attentes sont donc "successives", alors qu'il serait bien plus intéressant qu'elles soient "simultanées". Si c'était le cas, l'utilisateur pourrait expédier des messages à tout moment, sans devoir attendre à chaque fois la réaction de son partenaire. Il pourrait également recevoir n'importe quel nombre de messages, sans l'obligation d'avoir à répondre à chacun d'eux pour recevoir les autres.
Nous pouvons arriver à ce résultat si nous apprenons à gérer plusieurs séquences d'instructions "en parallèle" au sein d'un même programme. Mais comment cela est-il possible ?
Au cours de l'histoire de l'informatique, plusieurs techniques ont été mises au point pour partager le temps de travail d'un processeur entre différentes tâches, de telle manière que celles-ci paraissent être effectuées en même temps (alors qu'en réalité le processeur s'occupe d'un petit bout de chacune d'elles à tour de rôle). Ces techniques sont implémentées dans le système d'exploitation, et il n'est pas nécessaire de les détailler ici, même s'il est possible d'accéder à chacune d'elles avec Python.
Dans les pages suivantes, nous allons apprendre à utiliser celle de ces techniques qui est à la fois la plus facile à mettre en œuvre, et la seule qui soit véritablement portable (elle est en effet supportée par tous les grands systèmes d'exploitation) : on l'appelle la technique des processus légers ou threads.
Dans un programme d'ordinateur, les threads sont des flux d'instructions qui sont menés en parallèle (quasi-simultanément), tout en partageant le même espace de noms global.
En fait, le flux d'instructions de n'importe quel programme Python suit toujours au moins un thread : le "thread principal".
À partir de celui-ci, d'autres threads « enfants » peuvent être amorcés, qui seront exécutés en parallèle. Chaque thread enfant se termine et disparaît sans autre forme de procès lorsque toutes les instructions qu'il contient ont été exécutées. Par contre, lorsque le thread principal se termine, il faut parfois s'assurer que tous ses threads enfants « meurent » avec lui. 
Client gérant l'émission et la réception simultanées.
Nous allons maintenant mettre en pratique la technique des threads pour construire un système de « chat » simplifié. Ce système sera constitué d'un seul serveur et d'un nombre quelconque de clients. Contrairement à ce qui se passait dans notre premier exercice, personne n'utilisera le serveur lui-même pour communiquer, mais lorsque celui-ci aura été mis en route, plusieurs clients pourront s'y connecter et commencer à s'échanger des messages.
Chaque client enverra tous ses messages au serveur, mais celui-ci les ré-expédiera immédiatement à tous les autres clients connectés, de telle sorte que chacun puisse voir l'ensemble du trafic. Chacun pourra à tout moment envoyer ses messages, et recevoir ceux des autres, dans n'importe quel ordre, la réception et l'émission étant gérées simultanément, dans des threads séparés.
Le script ci-après définit le programme client. Vous constaterez que la partie principale du script (ligne 38 et suivantes) est similaire à celle de l'exemple précédent. Seule la partie « Dialogue avec le serveur » a été remplacée. Au lieu d'une boucle codice_2, vous y trouvez à présent les instructions de création de deux objets threads (aux lignes 49 et 50), dont on démarre la fonctionnalité aux deux lignes suivantes. Ces objets threads sont créés par dérivation, à partir de la classe codice_4 du module "threading". Ils s'occuperont indépendamment de la réception et de l'émission des messages. Les deux threads « enfants » sont ainsi parfaitement encapsulés dans des objets distincts, ce qui facilite la compréhension du mécanisme.
host = '192.168.0.235'
port = 40000
import socket, sys, threading
class ThreadReception(threading.Thread):
 """objet thread gérant la réception des messages"""
 def __init__(self, conn):
 threading.Thread.__init__(self)
 self.connexion = conn # réf. du socket de connexion
 def run(self):
 while 1:
 message_recu = self.connexion.recv(1024)
 print "*" + message_recu + "*"
 if message_recu ==" or message_recu.upper() == "FIN":
 break
 # Le thread <réception> se termine ici.
 # On force la fermeture du thread <émission> :
 th_E._Thread__stop()
 print "Client arrêté. Connexion interrompue."
 self.connexion.close()
class ThreadEmission(threading.Thread):
 """objet thread gérant l'émission des messages"""
 def __init__(self, conn):
 threading.Thread.__init__(self)
 self.connexion = conn # réf. du socket de connexion
 def run(self):
 while 1:
 message_emis = raw_input()
 self.connexion.send(message_emis)
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
 connexion.connect((host, port))
except socket.error:
 print "La connexion a échoué."
 sys.exit() 
print "Connexion établie avec le serveur."
th_E = ThreadEmission(connexion)
th_R = ThreadReception(connexion)
th_E.start()
th_R.start() 
Remarque générale : Dans cet exemple, nous avons décidé de créer deux objets threads indépendants du thread principal, afin de bien mettre en évidence les mécanismes. Notre programme utilise donc trois threads en tout, alors que le lecteur attentif aura remarqué que deux pourraient suffire. En effet : le thread principal ne sert en définitive qu'à lancer les deux autres ! Il n'y a cependant aucun intérêt à limiter le nombre de threads. Au contraire : à partir du moment où l'on décide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unités bien distinctes.
Ligne 7 : Le module "threading" contient la définition de toute une série de classes intéressantes pour gérer les threads. Nous n'utiliserons ici que la seule classe codice_4, lorsque nous devrons nous préoccuper de problèmes de synchronisation entre différents threads concurrents.
Lignes 9 à 25 : Les classes dérivées de la classe codice_4 contiendront essentiellement une méthode codice_7. C'est dans celle-ci que l'on placera la portion de programme spécifiquement confiée au thread. Il s'agira souvent d'une boucle répétitive, comme ici. Vous pouvez parfaitement considérer le contenu de cette méthode comme un script indépendant, qui s'exécute en parallèle avec les autres composants de votre application. Lorsque ce code a été complètement exécuté, le thread se referme.
Lignes 16 à 20 : Cette boucle gère la réception des messages. À chaque itération, le flux d'instructions s'interrompt à la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas figé pour autant : les autres threads continuent leur travail indépendamment.
Ligne 19 : La sortie de boucle est provoquée par la réception d'un message codice_8 (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupée par le partenaire). Quelques instructions de « nettoyage » sont alors exécutées, et puis le thread se termine.
Ligne 23 : Lorsque la réception des messages est terminée, nous souhaitons que le reste du programme se termine lui aussi. Il nous faut donc forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gérer l'émission des messages. Cette fermeture forcée peut être obtenue à l'aide de la méthode codice_9.
Lignes 27 à 36 : Cette classe définit donc un autre objet thread, qui contient cette fois une boucle de répétition perpétuelle. Il ne pourra donc se terminer que contraint et forcé par méthode décrite au paragraphe précédent. À chaque itération de cette boucle, le flux d'instructions s'interrompt à la ligne 35 dans l'attente d'une entrée clavier, mais cela n'empêche en aucune manière les autres threads de faire leur travail.
Lignes 38 à 45 : Ces lignes sont reprises à l'identique des scripts précédents.
Lignes 47 à 52 : Instanciation et démarrage des deux objets threads « enfants ». Veuillez noter qu'il est recommandé de provoquer ce démarrage en invoquant la méthode intégrée codice_10, plutôt qu'en faisant appel directement à la méthode codice_7 que vous aurez définie vous-même. Sachez également que vous ne pouvez invoquer codice_10 qu'une seule fois (une fois arrêté, un objet thread ne peut pas être redémarré).
Serveur gérant les connexions de plusieurs clients en parallèle.
Le script ci-après crée un serveur capable de prendre en charge les connexions d'un certain nombre de clients du même type que ce que nous avons décrit dans les pages précédentes.
Ce serveur n'est pas utilisé lui-même pour communiquer : ce sont les clients qui communiquent les uns avec les autres, par l'intermédiaire du serveur. Celui-ci joue donc le rôle d'un relais : il accepte les connexions des clients, puis attend l'arrivée de leurs messages. Lorsqu'un message arrive en provenance d'un client particulier, le serveur le ré-expédie à tous les autres, en lui ajoutant au passage une chaîne d'identification spécifique du client émetteur, afin que chacun puisse voir tous les messages, et savoir de qui ils proviennent.
HOST = '192.168.0.235'
PORT = 40000
import socket, sys, threading
class ThreadClient(threading.Thread):
 dérivation d'un objet thread pour gérer la connexion avec un client
 def __init__(self, conn):
 threading.Thread.__init__(self)
 self.connexion = conn
 def run(self):
 # Dialogue avec le client :
 nom = self.getName() # Chaque thread possède un nom
 while 1:
 msgClient = self.connexion.recv(1024)
 if msgClient.upper() == "FIN" or msgClient =="":
 break
 message = "%s> %s" % (nom, msgClient)
 print message
 # Faire suivre le message à tous les autres clients :
 for cle in conn_client:
 if cle != nom: # ne pas le renvoyer à l'émetteur
 conn_client[cle].send(message)
 # Fermeture de la connexion :
 self.connexion.close() # couper la connexion côté serveur
 del conn_client[nom] # supprimer son entrée dans le dictionnaire
 print "Client %s déconnecté." % nom
 # Le thread se termine ici 
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
 mySocket.bind((HOST, PORT))
except socket.error:
 print "La liaison du socket à l'adresse choisie a échoué."
 sys.exit()
print "Serveur prêt, en attente de requêtes ..."
mySocket.listen(5)
conn_client = {} # dictionnaire des connexions clients
while 1: 
 connexion, adresse = mySocket.accept()
 # Créer un nouvel objet thread pour gérer la connexion :
 th = ThreadClient(connexion)
 th.start()
 # Mémoriser la connexion dans le dictionnaire : 
 it = th.getName() # identifiant du thread
 conn_client[it] = connexion
 print "Client %s connecté, adresse IP %s, port %s." %\
 (it, adresse[0], adresse[1])
 # Dialogue avec le client :
 connexion.send("Vous êtes connecté. Envoyez vos messages.")
Lignes 35 à 43 : L'initialisation de ce serveur est identique à celle du serveur rudimentaire décrit au début du présent chapitre.
Ligne 46 : Les références des différentes connexions doivent être mémorisées. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : La première est que nous devrons pouvoir ajouter ou enlever ces références dans n'importe quel ordre, puisque les clients se connecteront et se déconnecteront à leur guise. La seconde est que nous pouvons disposer aisément d'un identifiant unique pour chaque connexion, lequel pourra servir de clé d'accès dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par La classe codice_4.
Lignes 47 à 51 : Le programme commence ici une boucle de répétition perpétuelle, qui va constamment attendre l'arrivée de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet codice_14 est créé, lequel pourra s'occuper d'elle indépendamment de toutes les autres.
Lignes 52 à 54 : Obtention d'un identifiant unique à l'aide de la méthode codice_15. Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique à chaque nouveau thread : ce nom convient bien comme identifiant (ou clé) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaîne de caractères, de la forme : « Thread-N » (N étant le numéro d'ordre du thread).
Lignes 15 à 17 : Gardez bien à l'esprit qu'il se créera autant d'objets codice_14 que de connexions, et que tous ces objets fonctionneront en parallèle. La méthode codice_15 peut alors être utilisée au sein de l'un quelconque de ces objets pour retrouver son identité particulière. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26).
Lignes 18 à 23 : L'utilité du thread est de réceptionner tous les messages provenant d'un client particulier. Il faut donc pour cela une boucle de répétition perpétuelle, qui ne s'interrompra qu'à la réception du message spécifique : « fin », ou encore à la réception d'un message vide (cas où la connexion est coupée par le partenaire).
Lignes 24 à 27 : Chaque message reçu d'un client doit être ré-expédié à tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des clés du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-mêmes. Un simple test (à la ligne 26) nous évite de ré-expédier le message au client dont il provient.
Ligne 31 : Lorsque nous fermons un socket de connexion, il est préférable de supprimer sa référence dans le dictionnaire, puisque cette référence ne peut plus servir. Et nous pouvons faire cela sans précaution particulière, car les éléments d'un dictionnaire ne sont pas ordonnés (nous pouvons en ajouter ou en enlever dans n'importe quel ordre).
Jeu des bombardes, version réseau.
Au chapitre 15, nous avons commenté le développement d'un petit jeu de combat dans lequel des joueurs s'affrontaient à l'aide de bombardes. L'intérêt de ce jeu reste toutefois fort limité, tant qu'il se pratique sur un seul et même ordinateur. Nous allons donc le perfectionner, en y intégrant les techniques que nous venons d'apprendre. Comme le système de « chat » décrit dans les pages précédentes, l'application complète se composera désormais de deux programmes distincts : un logiciel serveur qui ne sera mis en fonctionnement que sur une seule machine, et un logiciel client qui pourra être lancé sur toute une série d'autres. Du fait du caractère portable de Python, il vous sera même possible d'organiser des combats de bombardes entre ordinateurs gérés par des systèmes d'exploitation différents (MacOS <> Linux <> Windows !).
Programme serveur : vue d'ensemble.
Les programmes serveur et client exploitent la même base logicielle, elle-même largement récupérée de ce qui avait déjà été mis au point tout au long du chapitre 15. Nous admettrons donc pour la suite de cet exposé que les deux versions précédentes du jeu ont été sauvegardées dans les fichiers-modules codice_18 et codice_19, installés dans le répertoire courant. Nous pouvons en effet réutiliser une bonne partie du code qu'ils contiennent, en nous servant judicieusement de l'importation et de l'héritage de classes. 
Du module "canon04", nous allons réutiliser la classe codice_20 telle quelle, aussi bien pour le logiciel serveur que pour le logiciel client. De ce même module, nous importerons également la classe codice_21, dont nous ferons dériver la classe maîtresse de notre application serveur : codice_22.
Du module "canon03", nous récupérerons la classe codice_23 dont nous tirerons une version plus adaptée au « contrôle à distance ».
Enfin, deux nouvelles classes viendront s'ajouter aux précédentes, chacune spécialisée dans la création d'un objet thread : la classe codice_24, dont une instance surveillera en permanence le socket destiné à réceptionner les demandes de connexion de nouveaux clients, et la classe codice_25, qui servira à créer autant d'objets sockets que nécessaire pour assurer le dialogue avec chacun des clients déjà connectés.
Ces nouvelles classes seront inspirées de celles que nous avions développées pour notre serveur de "chat" dans les pages précédentes. La principale différence par rapport à celui-ci est que nous devrons activer un thread spécifique pour le code qui gère l'attente et la prise en charge des connexions clientes, afin que l'application principale puisse faire autre chose pendant ce temps.
À partir de là, notre plus gros travail consistera à "développer un protocole de communication" pour le dialogue entre le serveur et ses clients. De quoi est-il question ? Tout simplement de définir la teneur des messages que vont s'échanger les machines connectées. Rassurez-vous : la mise au point de ce « langage » peut être progressive. On commence par établir un dialogue de base, puis on y ajoute petit à petit un « vocabulaire » plus étendu.
L'essentiel de ce travail peut être accompli en s 'aidant du logiciel client développé précédemment pour le système de "chat". On se sert de celui-ci pour envoyer des « ordres » au serveur en cours de développement, et on corrige celui-ci jusqu'à ce qu'il « obéisse » : en clair, les procédures que l'on met en place progressivement sur le serveur sont testées au fur et à mesure, en réponse aux messages correspondants émis « à la main » à partir du client.
Protocole de communication.
Il va de soi que le protocole décrit ci-après est tout à fait arbitraire. Il serait parfaitement possible de choisir d'autres conventions complètement différentes. Vous pouvez bien évidemment critiquer les choix effectués, et vous souhaiterez peut-être même les remplacer par d'autres, plus efficients ou plus simples.
Vous savez déjà que les messages échangés sont de simples chaînes de caractères. Prévoyant que certains de ces messages devront transmettre plusieurs informations à la fois, nous avons décidé que chacun d'eux pourrait comporter plusieurs champs, que nous séparerons à l'aide de virgules. Lors de la réception de l'un quelconque de ces messages, nous pourrons alors aisément récupérer tous ses composants dans une liste, à l'aide de la méthode intégrée codice_26.
Voici un exemple de dialogue type, tel qu'il peut être suivi du côté d'un client. Les messages entre astérisques sont ceux qui sont reçus du serveur ; les autres sont ceux qui sont émis par le client lui-même :
client OK
OK
orienter,25,
feu
feu
Lorsqu'un nouveau client démarre, il envoie une requête de connexion au serveur, lequel lui expédie en retour le message : « serveur OK ». À la réception de ce dernier, le client répond alors en envoyant lui-même : « client OK ». Ce premier échange de politesses n'est pas absolument indispensable, mais il permet de vérifier que la communication passe bien dans les deux sens. Étant donc averti que le client est prêt à travailler, le serveur lui expédie alors une description des canons déjà présents dans le jeu (éventuellement aucun) : identifiant, emplacement sur le canevas, orientation et couleur (ligne 3).
En réponse à l'accusé de réception du client (ligne 4), le serveur installe un nouveau canon dans l'espace de jeu, puis il signale les caractéristiques de cette installation non seulement au client qui l'a provoquée, mais également à tous les autres clients connectés. Le message expédié au nouveau client comporte cependant une différence (car c'est lui le propriétaire de ce nouveau canon) : en plus des caractéristiques du canon, qui sont fournies à tout le monde, il comporte un champ supplémentaire contenant simplement « le_vôtre » (comparez par exemple la ligne 5 avec la ligne 17, laquelle signale la connexion d'un autre joueur). Cette indication supplémentaire permet au client propriétaire du canon de distinguer parmi plusieurs messages similaires éventuels, celui qui contient l'identifiant unique que lui a attribué le serveur.
Les messages des lignes 6 et 7 sont des commandes envoyées par le client (réglage de la hausse et commande de tir). Dans la version précédente du jeu, nous avions déjà convenu que les canons se déplaceraient quelque peu (et au hasard) après chaque tir. Le serveur effectue donc cette opération, et s'empresse ensuite d'en faire connaître le résultat à tous les clients connectés. Le message reçu du serveur à la ligne 8 est donc l'indication d'un tel déplacement (les coordonnées fournies sont les coordonnées résultantes pour le canon concerné).
La ligne 11 reproduit le type de message expédié par le serveur lorsqu'une cible a été touchée. Les nouveaux scores de tous les joueurs sont ainsi communiqués à tous les clients.
Les messages serveur des lignes 12, 13 et 14 indiquent les actions entreprises par un autre joueur (réglage de hausse suivi d'un tir). Cette fois encore, le canon concerné est déplacé au hasard après qu'il ait tiré (ligne 15).
Lignes 16 et 17 : lorsque l'un des clients coupe sa connexion, le serveur en avertit tous les autres, afin que le canon correspondant disparaisse de l'espace de jeu sur tous les postes. À l'inverse, de nouveaux clients peuvent se connecter à tout moment pour participer au jeu.
Le premier champ de chaque message indique sa teneur. Les messages envoyés par le client sont très simples : ils correspondent aux différentes actions entreprises par le joueur (modifications de l'angle de tir et commandes de feu). Ceux qui sont envoyés par le serveur sont un peu plus complexes. La plupart d'entre eux sont expédiés à tous les clients connectés, afin de les tenir informés du déroulement du jeu. En conséquence, ces messages doivent mentionner l'identifiant du joueur qui a commandé une action ou qui est concerné par un changement quelconque. Nous avons vu plus haut que ces identifiants sont des noms générés automatiquement par le gestionnaire de threads du serveur, chaque fois qu'un nouveau client se connecte.
Certains messages concernant l'ensemble du jeu contiennent plusieurs informations par champ. Dans ce cas, les différents « sous-champs » sont séparés par des points-virgules (lignes 3 et 11).
Programme serveur : première partie.
Vous trouverez dans les pages qui suivent le script complet du programme serveur. Nous vous le présentons en trois morceaux successifs afin de rapprocher les commentaires du code correspondant, mais la numérotation de ses lignes est continue. Bien qu'il soit déjà relativement long et complexe, vous estimerez probablement qu'il mérite d'être encore perfectionné, notamment au niveau de la présentation générale. Nous vous laisserons le soin d'y ajouter vous-même tous les compléments qui vous sembleront utiles (par exemple, une proposition de choisir les coordonnées de la machine hôte au démarrage, une barre de menus, etc.) :
host, port = '192.168.0.235', 35000
largeur, hauteur = 700, 400 # dimensions de l'espace de jeu
from Tkinter import *
import socket, sys, threading, time
import canon03
from canon04 import Canon, AppBombardes
class Pupitre(canon03.Pupitre):
 """Pupitre de pointage amélioré""" 
 def __init__(self, boss, canon):
 canon03.Pupitre.__init__(self, boss, canon)
 def tirer(self):
 "déclencher le tir du canon associé"
 self.appli.tir_canon(self.canon.id)
 def orienter(self, angle):
 "ajuster la hausse du canon associé"
 self.appli.orienter_canon(self.canon.id, angle)
 def valeur_score(self, sc =None):
 "imposer un nouveau score <sc>, ou lire le score existant"
 if sc == None:
 return self.score
 else:
 self.score =sc
 self.points.config(text = ' %s ' % self.score)
 def inactiver(self):
 "désactiver le bouton de tir et le système de réglage d'angle"
 self.bTir.config(state =DISABLED)
 self.regl.config(state =DISABLED) 
 def activer(self):
 "activer le bouton de tir et le système de réglage d'angle"
 self.bTir.config(state =NORMAL)
 self.regl.config(state =NORMAL)
 def reglage(self, angle):
 "changer la position du curseur de réglage"
 self.regl.config(state =NORMAL)
 self.regl.set(angle)
 self.regl.config(state =DISABLED)
La classe codice_23 est construite par dérivation de la classe de même nom importée du module "canon03". Elle hérite donc toutes les caractéristiques de celle-ci, mais nous devons surcharger ses méthodes codice_28 et codice_29 :
Dans la version monoposte du logiciel, en effet, chacun des pupitres pouvait commander directement l'objet canon correspondant. Dans cette version réseau, par contre, ce sont les clients qui contrôlent à distance le fonctionnement des canons. Par conséquent, les pupitres qui apparaissent dans la fenêtre du serveur ne peuvent être que de simples répétiteurs des manœuvres effectuées par les joueurs sur chaque client. Le bouton de tir et le curseur de réglage de la hausse sont donc désactivés, mais les indications fournies obéissent aux injonctions qui leur sont adressées par l'application principale.
Cette nouvelle classe codice_23 sera également utilisée telle quelle dans chaque exemplaire du programme client. Dans la fenêtre de celui-ci comme dans celle du serveur, tous les pupitres seront affichés comme des répétiteurs, mais l'un d'entre eux cependant sera complètement fonctionnel : celui qui correspond au canon du joueur.
Toutes ces raisons expliquent également l'apparition des nouvelles méthodes : codice_31, codice_32, codice_33 et codice_34, qui seront elles aussi invoquées par l'application principale, en réponse aux messages-instructions échangés entre le serveur et ses clients.
La classe codice_25 ci-dessous sert à instancier la série d'objets threads qui s'occuperont en parallèle de toutes les connexions lancées par les clients. Sa méthode codice_7 contient la fonctionnalité centrale du serveur, à savoir la boucle d'instructions qui gère la réception des messages provenant d'un client particulier, lesquels entraînent chacun toute une cascade de réactions. Vous y trouverez la mise en œuvre concrète du protocole de communication décrit dans les pages précédentes.
class ThreadConnexion(threading.Thread):
 """objet thread gestionnaire d'une connexion client"""
 def __init__(self, boss, conn):
 threading.Thread.__init__(self)
 self.connexion = conn # réf. du socket de connexion
 self.app = boss # réf. de la fenêtre application
 def run(self):
 "actions entreprises en réponse aux messages reçus du client"
 nom = self.getName() # id. du client = nom du thread
 while 1:
 msgClient = self.connexion.recv(1024)
 print "**%s** de %s" % (msgClient, nom)
 deb = msgClient.split(',')[0]
 if deb == "fin" or deb =="":
 self.app.enlever_canon(nom)
 # signaler le départ de ce canon aux autres clients :
 self.app.verrou.acquire()
 for cli in self.app.conn_client:
 if cli != nom:
 message = "départ_de,%s" % nom
 self.app.conn_client[cli].send(message)
 self.app.verrou.release() 
 # fermer le présent thread :
 break 
 elif deb =="client OK":
 # signaler au nouveau client les canons déjà enregistrés :
 msg ="canons,"
 for g in self.app.guns:
 gun = self.app.guns[g]
 msg =msg +"%s;%s;%s;%s;%s," % \
 (gun.id, gun.x1, gun.y1, gun.sens, gun.coul)
 self.app.verrou.acquire()
 self.connexion.send(msg)
 # attendre un accusé de réception ('OK') :
 self.connexion.recv(100)
 self.app.verrou.release() 
 # ajouter un canon dans l'espace de jeu serveur.
 # la méthode invoquée renvoie les caract. du canon créé :
 x, y, sens, coul = self.app.ajouter_canon(nom)
 # signaler les caract. de ce nouveau canon à tous les
 # clients déjà connectés :
 self.app.verrou.acquire()
 for cli in self.app.conn_client:
 msg ="nouveau_canon,%s,%s,%s,%s,%s" % \
 (nom, x, y, sens, coul)
 # pour le nouveau client, ajouter un champ indiquant
 # que le message concerne son propre canon :
 if cli == nom:
 msg =msg +",le_vôtre"
 self.app.conn_client[cli].send(msg)
 self.app.verrou.release()
 elif deb =='feu':
 self.app.tir_canon(nom)
 # Signaler ce tir à tous les autres clients :
 self.app.verrou.acquire()
 for cli in self.app.conn_client:
 if cli != nom:
 message = "tir_de,%s," % nom
 self.app.conn_client[cli].send(message) 
 self.app.verrou.release()
 elif deb =="orienter":
 t =msgClient.split(',')
 # on peut avoir reçu plusieurs angles. utiliser le dernier: 
 self.app.orienter_canon(nom, t[-2])
 # Signaler ce changement à tous les autres clients :
 self.app.verrou.acquire()
 for cli in self.app.conn_client:
 if cli != nom:
 # virgule terminale, car messages parfois groupés :
 message = "angle,%s,%s," % (nom, t[-2])
 self.app.conn_client[cli].send(message)
 self.app.verrou.release()
 # Fermeture de la connexion :
 self.connexion.close() # couper la connexion
 del self.app.conn_client[nom] # suppr. sa réf. dans le dictionn.
 self.app.afficher("Client %s déconnecté.\n" % nom)
 # Le thread se termine ici
Synchronisation de threads concurrents à l'aide de « verrous » (thread locks).
Au cours de votre examen du code ci-dessus, vous aurez certainement remarqué la structure particulière des blocs d'instructions par lesquelles le serveur expédie un même message à tous ses clients. Considérez par exemple les lignes 74 à 80 :
La ligne 75 active la méthode codice_37 d'un objet « verrou » qui a été créé par le constructeur de l'application principale. Cet objet est une instance de la classe codice_38, laquelle fait partie du module "threading" que nous avons importé en début de script. Les lignes suivantes (76 à 79) provoquent l'envoi d'un message à tous les clients connectés (sauf un). Ensuite, l'objet « verrou » est à nouveau sollicité, cette fois pour sa méthode codice_39.
À quoi cet objet « verrou » peut-il donc bien servir ? Puisqu'il est produit par une classe du module "threading", vous pouvez deviner que son utilité concerne les threads. En fait, de tels objets « verrous » servent à "synchroniser les threads concurrents". De quoi s'agit-il ?
Vous savez que le serveur démarre un thread différent pour chacun des clients qui se connecte. Ensuite, tous ces threads fonctionnent en parallèle. Il existe donc un risque que de temps à autre, deux ou plusieurs de ces threads essaient d'utiliser une ressource commune en même temps.
Dans les lignes de code que nous venons de discuter, par exemple, nous avons affaire à un thread qui souhaite exploiter quasiment toutes les connexions présentes pour poster un message. Il est donc parfaitement possible que pendant ce temps, un autre thread tente d'exploiter lui aussi l'une ou l'autre de ces connexions, ce qui risque de provoquer un dysfonctionnement (en l'occurrence, la superposition chaotique de plusieurs messages).
Un tel problème de "concurrence entre threads" peut être résolu par l'utilisation d'un objet-verrou (thread lock). Un tel objet n'est créé qu'en un seul exemplaire, dans un espace de noms accessible à tous les threads concurrents. Il se caractérise essentiellement par le fait qu'il se trouve toujours dans l'un ou l'autre de deux états : soit verrouillé, soit déverrouillé. Son état initial est l'état déverrouillé.
Lorsqu'un thread quelconque s'apprête à accéder à une ressource commune, il active d'abord la méthode codice_37 du verrou. Si celui-ci était dans l'état déverrouillé, il se verrouille, et le thread demandeur peut alors utiliser la ressource commune, en toute tranquillité. Lorsqu'il aura fini d'utiliser la ressource, il s'empressera cependant d'activer la méthode codice_39 du verrou, ce qui le fera repasser dans l'état déverrouillé.
En effet : Si un autre thread concurrent active lui aussi la méthode codice_37 du verrou, alors que celui-ci est dans l'état verrouillé, la méthode « ne rend pas la main », provoquant le blocage de ce thread, lequel suspend donc son activité jusqu'à ce que le verrou repasse dans l'état déverrouillé. Ceci l'empêche donc d'accéder à la ressource commune durant tout le temps où un autre thread s'en sert. Lorsque le verrou est déverrouillé, l'un des threads en attente (il peut en effet y en avoir plusieurs) reprend alors son activité, et ainsi de suite.
L'objet verrou mémorise les références des threads bloqués, de manière à n'en débloquer qu'un seul à la fois lorsque sa méthode codice_39 est invoquée. Il faut donc toujours veiller à ce que chaque thread qui active la méthode acquire() du verrou avant d'accéder à une ressource, active également sa méthode codice_39 peu après.
Pour autant que tous les threads concurrents respectent la même procédure, cette technique simple empêche donc qu'une ressource commune soit exploitée en même temps par plusieurs d'entre eux. On dira dans ce cas que les threads ont été "synchronisés".
Programme serveur : suite et fin.
Les deux classes ci-dessous complètent le script serveur. Le code implémenté dans la classe codice_24 est assez similaire à celui que nous avions développé précédemment pour le corps d'application du logiciel de « Chat ». Dans le cas présent, toutefois, nous le plaçons dans une classe dérivée de codice_4, parce que devons faire fonctionner ce code dans un thread indépendant de celui de l'application principale. Celui-ci est en effet déjà complètement accaparé par la boucle codice_47 de l'interface graphique.
La classe codice_22 dérive de la classe codice_21 du module "canon04". Nous lui avons ajouté un ensemble de méthodes complémentaires destinées à exécuter toutes les opérations qui résulteront du dialogue entamé avec les clients. Nous avons déjà signalé plus haut que les clients instancieront chacun une version dérivée de cette classe (afin de profiter des mêmes définitions de base pour la fenêtre, le canevas, etc.).
class ThreadClients(threading.Thread):
 """objet thread gérant la connexion de nouveaux clients"""
 def __init__(self, boss, connex):
 threading.Thread.__init__(self)
 self.boss = boss # réf. de la fenêtre application
 self.connex = connex # réf. du socket initial
 def run(self):
 "attente et prise en charge de nouvelles connexions clientes"
 txt ="Serveur prêt, en attente de requêtes ...\n"
 self.boss.afficher(txt)
 self.connex.listen(5) 
 # Gestion des connexions demandées par les clients :
 while 1: 
 nouv_conn, adresse = self.connex.accept()
 # Créer un nouvel objet thread pour gérer la connexion :
 th = ThreadConnexion(self.boss, nouv_conn)
 th.start()
 it = th.getName() # identifiant unique du thread
 # Mémoriser la connexion dans le dictionnaire :
 self.boss.enregistrer_connexion(nouv_conn, it)
 # Afficher :
 txt = "Client %s connecté, adresse IP %s, port %s.\n" %\
 (it, adresse[0], adresse[1])
 self.boss.afficher(txt)
 # Commencer le dialogue avec le client :
 nouv_conn.send("serveur OK")
class AppServeur(AppBombardes):
 """fenêtre principale de l'application (serveur ou client)"""
 def __init__(self, host, port, larg_c, haut_c):
 self.host, self.port = host, port
 AppBombardes.__init__(self, larg_c, haut_c) 
 self.active =1 # témoin d'activité
 # veiller à quitter proprement si l'on referme la fenêtre :
 self.bind('<Destroy>',self.fermer_threads)
 def specificites(self):
 "préparer les objets spécifiques de la partie serveur" 
 self.master.title('«< Serveur pour le jeu des bombardes »>')
 # widget Text, associé à une barre de défilement :
 st =Frame(self)
 self.avis =Text(st, width =65, height =5)
 self.avis.pack(side =LEFT)
 scroll =Scrollbar(st, command =self.avis.yview)
 self.avis.configure(yscrollcommand =scroll.set)
 scroll.pack(side =RIGHT, fill =Y)
 st.pack()
 # partie serveur réseau :
 self.conn_client = {} # dictionn. des connexions clients
 self.verrou =threading.Lock() # verrou pour synchroniser threads
 # Initialisation du serveur - Mise en place du socket :
 connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 try:
 connexion.bind((self.host, self.port))
 except socket.error:
 txt ="La liaison du socket à l'hôte %s, port %s a échoué.\n" %\
 (self.host, self.port)
 self.avis.insert(END, txt)
 self.accueil =None
 else:
 # démarrage du thread guettant la connexion des clients :
 self.accueil = ThreadClients(self, connexion)
 self.accueil.start()
 def depl_aleat_canon(self, id):
 "déplacer aléatoirement le canon <id>"
 x, y = AppBombardes.depl_aleat_canon(self, id)
 # signaler ces nouvelles coord. à tous les clients :
 self.verrou.acquire()
 for cli in self.conn_client:
 message = "mouvement_de,%s,%s,%s," % (id, x, y)
 self.conn_client[cli].send(message)
 self.verrou.release()
 def goal(self, i, j):
 "le canon <i> signale qu'il a atteint l'adversaire <j>"
 AppBombardes.goal(self, i, j)
 # Signaler les nouveaux scores à tous les clients :
 self.verrou.acquire()
 for cli in self.conn_client:
 msg ='scores,'
 for id in self.pupi:
 sc = self.pupi[id].valeur_score()
 msg = msg +"%s;%s," % (id, sc)
 self.conn_client[cli].send(msg) 
 time.sleep(.5) # pour mieux séparer les messages 
 self.verrou.release()
 def ajouter_canon(self, id):
 "instancier un canon et un pupitre de nom <id> dans 2 dictionn."
 # on alternera ceux des 2 camps :
 n = len(self.guns)
 if n %2 ==0:
 sens = -1
 else:
 sens = 1
 x, y = self.coord_aleat(sens)
 coul =('dark blue', 'dark red', 'dark green', 'purple',
 'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet')[n]
 self.guns[id] = Canon(self.jeu, id, x, y, sens, coul)
 self.pupi[id] = Pupitre(self, self.guns[id])
 self.pupi[id].inactiver()
 return (x, y, sens, coul)
 def enlever_canon(self, id):
 "retirer le canon et le pupitre dont l'identifiant est <id>"
 if self.active == 0: # la fenêtre a été refermée
 return 
 self.guns[id].effacer()
 del self.guns[id]
 self.pupi[id].destroy()
 del self.pupi[id]
 def orienter_canon(self, id, angle):
 "régler la hausse du canon <id> à la valeur <angle>"
 self.guns[id].orienter(angle)
 self.pupi[id].reglage(angle) 
 def tir_canon(self, id):
 "déclencher le tir du canon <id>"
 self.guns[id].feu()
 def enregistrer_connexion(self, conn, it):
 "Mémoriser la connexion dans un dictionnaire"
 self.conn_client[it] = conn
 def afficher(self, txt):
 "afficher un message dans la zone de texte"
 self.avis.insert(END, txt)
 def fermer_threads(self, evt):
 "couper les connexions existantes et fermer les threads"
 # couper les connexions établies avec tous les clients :
 for id in self.conn_client:
 self.conn_client[id].send('fin')
 # forcer la terminaison du thread serveur qui attend les requêtes :
 if self.accueil != None:
 self.accueil._Thread__stop()
 self.active =0 # empêcher accès ultérieurs à Tk
if __name__ =='__main__':
 AppServeur(host, port, largeur, hauteur).mainloop()
Programme client.
Le script correspondant au logiciel client est reproduit ci-après. Comme celui qui correspond au serveur, il est relativement court, parce qu'il utilise lui aussi l'importation de modules et l'héritage de classes. Le script serveur doit avoir été sauvegardé dans un fichier-module nommé "canon_serveur.py". Ce fichier doit être placé dans le répertoire courant, de même que les fichiers-modules "canon03.py" et "canon04.py" qu'il utilise lui-même.
De ces modules ainsi importés, le présent script utilise les classes codice_20 et codice_23 à l'identique, ainsi qu'une forme dérivée de la classe codice_22. Dans cette dernière, de nombreuses méthodes ont été surchargées, afin d'adapter leur fonctionnalité. Considérez par exemple les méthodes codice_53 et codice_54, dont la variante surchargée ne fait plus rien du tout (instruction pass), parce que le calcul des scores et le repositionnement des canons après chaque tir ne peuvent être effectués que sur le serveur seulement.
C'est dans la méthode codice_7 de la classe codice_56 (lignes 86 à 126) que se trouve le code traitant les messages échangés avec le serveur. Nous y avons d'ailleurs laissé une instruction print (à la ligne 88) afin que les messages reçus du serveur apparaissent sur la sortie standard. Si vous réalisez vous-même une forme plus définitive de ce jeu, vous pourrez bien évidemment supprimer cette instruction.
from Tkinter import *
import socket, sys, threading, time
from canon_serveur import Canon, Pupitre, AppServeur 
host, port = '192.168.0.235', 35000
largeur, hauteur = 700, 400 # dimensions de l'espace de jeu
class AppClient(AppServeur):
 def __init__(self, host, port, larg_c, haut_c):
 AppServeur.__init__(self, host, port, larg_c, haut_c)
 def specificites(self):
 "préparer les objets spécifiques de la partie client" 
 self.master.title('«< Jeu des bombardes »>')
 self.connex =ThreadSocket(self, self.host, self.port)
 self.connex.start()
 self.id =None
 def ajouter_canon(self, id, x, y, sens, coul):
 "instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires"
 self.guns[id] = Canon(self.jeu, id, int(x),int(y),int(sens), coul)
 self.pupi[id] = Pupitre(self, self.guns[id])
 self.pupi[id].inactiver()
 def activer_pupitre_personnel(self, id):
 self.id =id # identifiant reçu du serveur
 self.pupi[id].activer()
 def tir_canon(self, id):
 r = self.guns[id].feu() # renvoie False si enrayé
 if r and id == self.id:
 self.connex.signaler_tir()
 def imposer_score(self, id, sc):
 self.pupi[id].valeur_score(int(sc))
 def deplacer_canon(self, id, x, y):
 "note: les valeurs de x et y sont reçues en tant que chaînes"
 self.guns[id].deplacer(int(x), int(y))
 def orienter_canon(self, id, angle):
 "régler la hausse du canon <id> à la valeur <angle>"
 self.guns[id].orienter(angle)
 if id == self.id:
 self.connex.signaler_angle(angle)
 else:
 self.pupi[id].reglage(angle)
 def fermer_threads(self, evt):
 "couper les connexions existantes et refermer les threads"
 self.connex.terminer()
 self.active =0 # empêcher accès ultérieurs à Tk
 def depl_aleat_canon(self, id):
 pass # => méthode inopérante
 def goal(self, a, b):
 pass # => méthode inopérante
class ThreadSocket(threading.Thread):
 """objet thread gérant l'échange de messages avec le serveur"""
 def __init__(self, boss, host, port):
 threading.Thread.__init__(self)
 self.app = boss # réf. de la fenêtre application
 # Mise en place du socket - connexion avec le serveur :
 self.connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 try:
 self.connexion.connect((host, port))
 except socket.error:
 print "La connexion a échoué."
 sys.exit() 
 print "Connexion établie avec le serveur."
 def run(self):
 while 1:
 msg_recu = self.connexion.recv(1024)
 print "*%s*" % msg_recu
 # le message reçu est d'abord converti en une liste :
 t =msg_recu.split(',')
 if t[0] =="" or t[0] =="fin":
 # fermer le présent thread :
 break 
 elif t[0] =="serveur OK":
 self.connexion.send("client OK")
 elif t[0] =="canons":
 self.connexion.send("OK") # accusé de réception
 # éliminons le 1er et le dernier élément de la liste.
 # ceux qui restent sont eux-mêmes des listes :
 lc = t[1:-1]
 # chacune est la description complète d'un canon :
 for g in lc:
 s = g.split(';')
 self.app.ajouter_canon(s[0], s[1], s[2], s[3], s[4])
 elif t[0] =="nouveau_canon":
 self.app.ajouter_canon(t[1], t[2], t[3], t[4], t[5])
 if len(t) >6:
 self.app.activer_pupitre_personnel(t[1])
 elif t[0] =='angle':
 # il se peut que l'on ait reçu plusieurs infos regroupées.
 # on ne considère alors que la première :
 self.app.orienter_canon(t[1], t[2]) 
 elif t[0] =="tir_de":
 self.app.tir_canon(t[1])
 elif t[0] =="scores":
 # éliminons le 1er et le dernier élément de la liste.
 # ceux qui restent sont eux-mêmes des listes :
 lc = t[1:-1]
 # chaque élément est la description d'un score :
 for g in lc:
 s = g.split(';')
 self.app.imposer_score(s[0], s[1])
 elif t[0] =="mouvement_de":
 self.app.deplacer_canon(t[1],t[2],t[3])
 elif t[0] =="départ_de":
 self.app.enlever_canon(t[1])
 # Le thread <réception> se termine ici.
 print "Client arrêté. Connexion interrompue."
 self.connexion.close()
 def signaler_tir(self):
 self.connexion.send('feu')
 def signaler_angle(self, angle):
 self.connexion.send('orienter,%s,' % angle)
 def terminer(self):
 self.connexion.send('fin')
if __name__ =='__main__':
 AppClient(host, port, largeur, hauteur).mainloop()
Lignes 105 à 108 : Un nouveau canon doit être ajouté dans l'espace de jeu de chacun (c'est-à-dire dans le canevas du serveur, et dans le canevas de tous les clients connectés), chaque fois qu'un nouveau client se connecte. Le serveur envoie donc à ce moment un même message à tous les clients pour les informer de la présence de ce nouveau partenaire. Mais le message envoyé à celui-ci en particulier comporte un champ supplémentaire (lequel contient simplement la chaîne « le_vôtre »), afin que ce partenaire sache que ce message concerne son propre canon, et qu'il puisse donc activer le pupitre correspondant, tout en mémorisant l'identifiant qui lui a été attribué par le serveur (voir également les lignes 35 à 37). 
Cette application vous a été présentée dans un but didactique. Nous y avons délibérément simplifié un certain nombre de problèmes. Par exemple, si vous testez vous-même ces logiciels, vous constaterez que les messages échangés sont souvent rassemblés en « paquets », ce qui nécessiterait d'affiner les algorithmes mis en place pour les interpréter. De même, nous avons à peine esquissé le mécanisme fondamental du jeu : répartition des joueurs dans les deux camps, destruction des canons touchés, obstacles divers, etc. Il vous reste bien des pistes à explorer !
Utilisation de threads pour optimiser les animations..
Le dernier exercice proposé à la fin de la section précédente nous suggère une méthodologie de développements d'applications qui peut se révéler particulièrement intéressante, dans le cas de jeux vidéo impliquant plusieurs animations simultanées.
En effet : si vous programmez les différents éléments animés d'un jeu comme des objets indépendants fonctionnant chacun sur son propre thread, alors non seulement vous vous simplifiez la tâche et vous améliorez la lisibilité de votre script, mais encore vous augmentez la vitesse d'exécution et donc la fluidité de ces animations. Pour arriver à ce résultat, vous devrez abandonner la technique de temporisation que vous avez exploitée jusqu'ici, mais celle que vous allez utiliser à sa place est finalement plus simple !
Temporisation des animations à l'aide de after().
Dans toutes les animations que nous avons décrites jusqu'à présent, le « moteur » était constitué à chaque fois par une fonction contenant la méthode codice_59, laquelle est associée d'office à tous les widgets "Tkinter". Vous savez que cette méthode permet d'introduire une temporisation dans le déroulement de votre programme : un chronomètre interne est activé, de telle sorte qu'après un intervalle de temps convenu, le système invoque automatiquement une fonction quelconque. En général, c'est la fonction contenant codice_59 qui est elle-même invoquée : on réalise ainsi une boucle récursive, dans laquelle il reste à programmer les déplacements des divers objets graphiques.
Vous devez bien comprendre que pendant l'écoulement de l'intervalle de temps programmé à l'aide de la méthode codice_59, votre application n'est pas du tout « figée ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenêtre, effectuer une entrée clavier, etc. Comment cela est-il rendu possible ?
Nous avons mentionné déjà à plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tâche de fond : ce dispositif se met en route lorsque vous activez la méthode codice_47 de votre fenêtre principale. Comme son nom l'indique fort bien, cette méthode met en œuvre une boucle répétitive perpétuelle, du même type que les boucles codice_2 que vous connaissez bien. De nombreux mécanismes sont intégrés à ce « moteur ». L'un d'entre eux consiste à réceptionner tous les événements qui se produisent, et à les signaler ensuite à l'aide de messages appropriés aux programmes qui en font la demande (voir : Programmes pilotés par des événements), d'autres contrôlent les actions à effectuer au niveau de l'affichage, etc. Lorsque vous faites appel à la méthode codice_59 d'un widget, vous utilisez en fait un mécanisme de chronométrage qui est intégré lui aussi à codice_47, et c'est donc ce gestionnaire central qui déclenche l'appel de fonction que vous souhaitez, après un certain intervalle de temps.
La technique d'animation utilisant la méthode codice_59 est la seule possible pour une application fonctionnant toute entière sur un seul thread, parce que c'est la boucle codice_47 qui dirige l'ensemble du comportement d'une telle application de manière absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenêtre chaque fois que cela s'avère nécessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redéfinirait les coordonnées d'un objet graphique à l'intérieur d'une simple boucle codice_2, par exemple, parce que pendant tout ce temps l'exécution de codice_47 resterait suspendue, ce qui aurait pour conséquence que pendant tout ce temps aucun objet graphique ne serait redessiné (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaîtrait figée, aussi longtemps que la boucle while ne serait pas interrompue.
Puisqu'elle est la seule possible, c'est donc cette technique que nous avons utilisée jusqu'à présent dans tous nos exemples d'applications mono-thread. Elle comporte cependant un inconvénient gênant : du fait du grand nombre d'opérations prises en charge à chaque itération de la boucle codice_47, la temporisation que l'on peut programmer à l'aide de codice_59 ne peut pas être très courte. Par exemple, elle ne peut guère descendre en dessous de 15 ms sur un PC typique (processeur de type Pentium IV, f = 1,5 GHz). Vous devez tenir compte de cette limitation si vous souhaitez développer des animations rapides.
Un autre inconvénient lié à l'utilisation de la méthode codice_59 réside dans la structure de la boucle d'animation (à savoir une fonction ou une méthode « récursive », c'est-à-dire qui s'appelle elle-même) : il n'est pas toujours simple en effet de bien maîtriser ce genre de construction logique, en particulier si l'on souhaite programmer l'animation de plusieurs objets graphiques indépendants, dont le nombre ou les mouvements doivent varier au cours du temps.
Temporisation des animations à l'aide de time.sleep().
Vous pouvez ignorer les limitations de la méthode codice_59 évoquées ci-dessus, si vous en confiez l'animation de vos objets graphiques à des threads indépendants. En procédant ainsi, vous vous libérez de la tutelle de codice_47, et il vous est permis alors de construire des procédures d'animation sur la base de structures de boucles plus « classiques », utilisant l'instruction codice_2 ou l'instruction for par exemple.
Au cœur de chacune de ces boucles, vous devez cependant toujours veiller à insérer une temporisation pendant laquelle vous « rendez la main » au système d'exploitation (afin qu'il puisse s'occuper des autres threads). Pour ce faire, vous ferez appel à la fonction codice_76 du module "time". Cette fonction permet de suspendre l'exécution du thread courant pendant un certain intervalle de temps, pendant lequel les autres threads et applications continuent à fonctionner. La temporisation ainsi produite ne dépend pas de codice_47, et par conséquent, elle peut être beaucoup plus courte que celle que vous autorise la méthode codice_59.
Attention : cela ne signifie pas que le rafraîchissement de l'écran sera lui-même plus rapide, car ce rafraîchissement continue à être assuré par codice_47. Vous pourrez cependant accélérer fortement les différents mécanismes que vous installez vous-même dans vos procédures d'animation. Dans un logiciel de jeu, par exemple, il est fréquent d'avoir à comparer périodiquement les positions de deux mobiles (tels qu' un projectile et une cible), afin de pouvoir entreprendre une action lorsqu'ils se rejoignent (explosion, ajout de points à un score, etc.). Avec la technique d'animation décrite ici, vous pouvez effectuer beaucoup plus souvent ces comparaisons et donc espérer un résultat plus précis. De même, vous pouvez augmenter le nombre de points pris en considération pour le calcul d'une trajectoire en temps réel, et donc affiner celle-ci.
Remarque : Lorsque vous utilisez la méthode codice_59, vous devez lui indiquer la temporisation souhaitée en millisecondes, sous la forme d'un argument entier. Lorsque vous faites appel à la fonction codice_76, par contre, l'argument que vous transmettez doit être exprimé en secondes, sous la forme d'un réel ("float"). Vous pouvez cependant utiliser des très petites valeurs (0.0003 par ex.).
Exemple concret.
Le petit script reproduit ci-dessous illustre la mise en œuvre de cette technique, dans un exemple volontairement minimaliste. Il s'agit d'une petite application graphique dans laquelle une figure se déplace en cercle à l'intérieur d'un canevas. Son « moteur » codice_47 est lancé comme d'habitude sur le thread principal. Le constructeur de l'application instancie un canevas contenant le dessin d'un cercle, un bouton et un objet thread. C'est cet objet thread qui assure l'animation du dessin, mais sans faire appel à la méthode codice_59 d'un widget. Il utilise plutôt une simple boucle codice_2 très classique, installée dans sa méthode codice_7.
from Tkinter import *
from math import sin, cos
import time, threading
class App(Frame):
 def __init__(self):
 Frame.__init__(self)
 self.pack()
 can =Canvas(self, width =400, height =400,
 bg ='ivory', bd =3, relief =SUNKEN)
 can.pack(padx =5, pady =5)
 cercle = can.create_oval(185, 355, 215, 385, fill ='red')
 tb = Thread_balle(can, cercle)
 Button(self, text ='Marche', command =tb.start).pack(side =LEFT)
 # Button(self, text ='Arrêt', command =tb.stop).pack(side =RIGHT)
 # arrêter l'autre thread si l'on ferme la fenêtre :
 self.bind('<Destroy>', tb.stop)
class Thread_balle(threading.Thread):
 def __init__(self, canevas, dessin):
 threading.Thread.__init__(self)
 self.can, self.dessin = canevas, dessin
 self.anim =1
 def run(self):
 a = 0.0
 while self.anim == 1:
 a += .01
 x, y = 200 + 170*sin(a), 200 +170*cos(a)
 self.can.coords(self.dessin, x-15, y-15, x+15, y+15)
 time.sleep(0.010)
 def stop(self, evt =0):
 self.anim =0
App().mainloop()