• 20 heures
  • Facile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

J'ai tout compris !

Mis à jour le 20/12/2017

Les threads

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous avons les clés en mains pour effectuer une complète communication entre deux programmes via le réseau. Pas mal en effet, toutefois, admettez qu'il n'y a pas souvent des programmes dédiés uniquement à la communication (à part un serveur, à la limite). Il va donc falloir adapter nos programmes afin de les rendre capables de faire plusieurs choses à la fois : leur travail habituel et la connexion réseau. Et ce n'est pas aussi simple qu'il n'y paraît !

Heureusement, les threads vont nous aider à résoudre ce petit problème.

Introduction

Commençons par le commencement, qu'est-ce qu'un thread et à quoi va-t-il bien pouvoir vous servir ?

Un thread est un processus. Par exemple, tous les programmes que nous avons faits jusqu'à maintenant ne contiennent qu'un seul processus, qu'un seul thread donc. Chaque ligne de code était exécutée après la précédente et ainsi de suite. Avec différents threads, les fonctions peuvent être appelées simultanément. Une fonction de calcul A et une fonction de calcul B peuvent être lancées simultanément. Cela n'a pas vraiment d’intérêt dans ce cas, car récupérer le résultat du thread va être très complexe. On va résumer cela en disant que lancer un thread revient à lancer un programme séparé. Par exemple, un thread ne pourra pas directement accéder aux composants visuels (interface graphique) du programme qui l'a créé, il va falloir effectuer des étapes particulières pour y accéder (nous y reviendrons plus tard).

C'est pas très concret, j'y comprends rien moi…

Bon, je vais vous donner un exemple où les threads vont être extrêmement utiles. Imaginons que notre programme demandant l'heure transmette l'heure au client non pas une fois, mais toutes les dix secondes (pas vraiment utile, mais c'est pour l'explication). Nous aurions un fonctionnement ressemblant à celui présenté à la figure suivante.

Fonctionnement du programme sans thread
Fonctionnement du programme sans thread

Dans ce schéma, vous voyez qu'une fois la connexion établie entre le client 1 et le serveur les deux effectuent le travail d'envoi/réception de l'heure et sont bloqués dans une boucle. Le client 2 qui veut se connecter, lui, ne pourra pas, car le serveur s'occupe désormais de l'envoi de l'heure, la phase d'acceptation client est passée, il n'y retournera plus tant que le client 1 sera connecté.

Imaginez maintenant que l'acceptation d'un client et la phase d'envoi de l'heure soient deux processus séparés. Alors, si le serveur crée un processus (thread) par client, chaque client pourra être « servi » de son côté. C'est ce que montre la figure suivante.

Fonctionnement du programme avec thread
Fonctionnement du programme avec thread

Ici par contre, le serveur a envoyé, une fois l'acceptation effectuée, chaque client vers un processus (thread) dédié à l'envoi de l'heure vers ledit client.
Ici, le serveur peut donc accepter un très grand nombre de clients et gérer chaque connexion de son côté.

En résumé…

Pour résumer tout ça, on utilisera les threads pour une tâche bloquante dans notre programme. Si on la lance dans un thread séparé, elle n'embêtera pas le traitement de notre programme. Ils peuvent être utilisés pour des tâches d'impression, de longs travaux sur les fichiers, des recherche, etc.
Vous avez sûrement parfois remarqué dans vos interfaces graphiques un « blocage », et si vous tentez d’interagir avec, vous obtenez une erreur du type « L'application ne répond pas… ». Cela arrive quand un long traitement est effectué sur le même thread que celui qui gère l'interface graphique.

Voilà donc pour résumer l’intérêt des threads. Allons tout de suite voir comment les mettre en œuvre.

Notre premier thread

Nous en avons fini avec la minute théorique. Désormais, nous allons nous concentrer sur la mise en œuvre des threads.

Pour créer un thread, il faut une fonction à partir de laquelle créer le processus. Dans l'exemple ci-dessus, la fonction qui sera appelée en tant que thread sera celle qui envoie l'heure toutes les dix secondes.

La mise en œuvre des threads est très rapide :

Dim MonThread As New Thread(AddressOf FonctionThread)
MonThread.Start()

Et l'import :

Imports System.Threading

Comme d'habitude, déclaration et instanciation de l'objet simultanément. Ici le paramètre du constructeur est AddressOf FonctionThread. On utilise le mot-clé AddressOf qui nous permet de récupérer l'adresse de la méthode que l'on souhaite appeler. Pour résumer, cela veut simplement dire que ce sera la méthode FonctionThread qui sera exécutée au démarrage du thread.
Le démarrage du thread ne s'effectue pas lors de la déclaration, il faut le démarrer en utilisant la méthode Start().

Il n'y a pas beaucoup de fonctions à connaître lorsque l'on manipule les threads. Je vais vous donner les vitales :

Suspend/Resume

Vous pouvez facilement mettre en pause et reprendre un thread.

MonThread.Suspend()

Ce code mettra en pause le thread MonThread. Son exécution ne continuera pas tant qu'un Resume ne sera pas appelé.

MonThread.Resume()

Ce code reprendra un thread en pause.

Join

Cette méthode vous sera utile. Elle attend la fin du thread désigné pour continuer l'exécution du programme. C'est donc une méthode bloquante.
Si dans le programme principal je lance un thread MonThread et que j'ai besoin de ses résultats de calcul pour continuer mon exécution, je peux écrire :

MonThread.Join()

Cette ligne ne sera donc pas passée tant que le thread MonThread ne sera pas terminé, autrement dit lorsque la fonction FonctionThread ne sera pas terminée (car c'est l'adresse de la fonction que j'ai passée au thread à la déclaration).

Abort

Je déconseille l'utilisation de cette méthode en fonctionnement normal. Cette méthode arrête brusquement un thread en cours d’exécution. Il est préférable de l'utiliser dans des cas d'urgence (fermeture du programme, erreur, etc.).

MonThread.Abort()

Ce code arrête l'exécution du thread.

Hey, comment je fais pour appeler des fonctions avec paramètres en utilisant ton AddressOf ?

En théorie vous ne pouvez pas. D'ailleurs, vous ne pouvez pas avoir un Return dans votre thread. Le seul moyen pour passer des paramètres à notre thread est de passer par une Class.
Il fallait y penser : vous instanciez une classe X avec trois membres privés. Le constructeur de cette fonction effectue leur attribution. Il vous reste à déclarer votre thread avec AddressOf X.MonFonction et cette fonction interne à la classe pourra alors utiliser les membres privés de cette dernière.

Et pour récupérer des valeurs résultant du thread, il faut les stocker dans un membre public de cette classe ou dans une variable globale. On en parle juste après.

La synchronisation

Un concept important pendant le développement de threads est la synchronisation.

Je m'explique : vous avez x threads de lancés et vous souhaitez partager des informations entre eux. Imaginez que vous ayez un thread qui s'occupe de lire une valeur en base de données toutes les y secondes et un autre qui doive effectuer un traitement sur cette variable.
Le problème ici est que, même si vous lancez le thread qui effectue la lecture avant celui qui effectue la modification, vous ne pouvez pas être certains de l'ordre d’exécution. La modification peut s'effectuer alors avant la lecture et là, badaboum ! Il faut mettre en place une synchronisation.

La variable globale

Première méthode très rapide à mettre en œuvre : la synchronisation par variable globale.
C'est tout simplement une variable à portée globale au module (donc en dehors d'une fonction) qui va être accessible même depuis nos threads. Les threads la modifieront pour voir si ce dernier peut effectuer son travail.

Avec le thread 1 qui veut lire en BDD et écrire dans X et le thread 2 qui veut traiter X. On les synchronise par la variable booléenne LectureTerminee :

  • Thread 2 : je veux traiter la variable X, est-ce que LectureTerminee est à True ? Non. Je patiente.

  • Thread 1 : j'ai terminé ma lecture en base de données, j'écris dans X et je mets LectureTerminee à True.

  • Thread 1 : je veux relire en base de données et réecrire la variable, est-ce que LectureTerminee est à False ? Non. Je patiente.

  • Thread 2 : je veux traiter la variable X, est-ce que LectureTerminee est à True ? Oui : je traite les données et je replace LectureTerminee à False.

  • Thread 1 : je veux relire en base de données et réecrire la variable, est-ce que LectureTerminee est à False ? Oui, je commence ma lecture en BDD.

  • Et ainsi de suite…

Quand les threads patientent, il faut bien sûr les faire entrer dans une boucle de type While ou Until en vérifiant la variable de temps en temps.

Attention, veillez bien à ne pas faire tourner le While ou le Until à l'infini, il faut lui faire faire des pauses avec :

Thread.Sleep(1000)

… où le paramètre est le temps d'attente en millisecondes (ici 1000 : 1 seconde).

Le SyncLock

Le SyncLock est lui plus utile pour vérifier qu'une variable ne sera pas modifiée par deux threads au même moment (on ne saurait alors plus où on en est), ou alors qu'elle est modifiée pendant le traitement d'un autre thread.
L'avantage du SyncLock est que les autres blocs tentant d'accéder à la même variable qu'un thread qui l'a déjà bloquée seront mis en attente. Exactement comme si on effectuait un While et des Sleep en attendant que cette variable change.

La mise en œuvre est particulière, il faut tout d'abord déclarer une variable qu'on va utiliser comme variable de contrôle, puis on peut verrouiller cette variable :

Dim VariableLock As Object = New Object()

Sub FonctionThread()

    SyncLock VariableLock 
         'Traitement d'une variable commune aux threads 
    End SyncLock

End Sub

Oui, c'est un bloc avec End. Et c'est justifié : pendant qu'on est à l'intérieur du SyncLock, on empêche les autres threads qui ont aussi effectué leur SyncLock d'entrer dans leur bloc. Une fois qu'un thread a atteint le End, un autre entre dedans et ainsi de suite…

SemaphoreSlim

L'objet SemaphoreSlim est encore un autre moyen de synchronisation. Il permet d'autoriser X threads de continuer leur exécution.

Le SemaphoreSlim est en fait une barrière de parking. Le parking a 10 places, si les 10 places sont occupées et qu'une voiture souhaite entrer, la barrière restera fermée. Si une voiture quitte le parking, une place se libère et la barrière s'ouvre. Une fois la voiture entrée, il n'y a de nouveau plus de place disponible et la barrière se referme. Et ainsi de suite.

La mise en œuvre nécessite encore une fois une variable globale, mais cette fois de type SemaphoreSlim :

Dim MonSemaphore As SemaphoreSlim = New SemaphoreSlim(10) 'Initialisation d'un SemaphoreSlim avec 10 places

Sub MonThread

    MonSemaphore.Wait() 'Attend qu'une place soit disponible
    'Une place est libre, l’exécution continue
    'Mon super traitement
    MonSemaphore.Release() 'Libère une place dans SemaphoreSlim pour les autres

End Sub

Mise en œuvre encore une fois très simple. Notez que le Wait() est une fonction bloquante. Il effectue à la fois l'attente de la libération du SemaphoreSlim et la prise d'une place lorsqu'une est disponible. Ne pas oublier le Release à la fin.

Les Windows Forms et les threads

Si vous avez fait vos tests de votre côté pendant ce tutoriel, vous avez sûrement eu des problèmes si vous avez voulu les interfacer avec les Windows Forms (l'interface graphique).

En effet, il y a un problème. Votre thread principal, le seul et l'unique, est celui qui s'occupe de toute la création et la gestion de l'interface graphique. C'est un peu comme un manager. Si un autre thread va vouloir accéder à ses objets graphiques (ses subordonnés), il va y avoir une erreur, un thread ne peut pas accéder aux ressources d'un autre thread, le manager ne veut pas qu'un autre manager vienne l’embêter dans son management.
Il va falloir effectuer une opération ninja pour aller modifier les ressources d'un autre thread, j'ai nommé cette opération l'opération Delegate (prononcez « diliguaite »).

Les delegates

Que sont donc ces delegates ? On peut traduire ça par « délégué » et je trouve que ça correspond bien à son rôle.
Le manager du thread secondaire, plutôt que d'aller donner directement des ordres aux subordonnés du manager principal, va lui donner des instructions pour gérer ses subordonnés. Il va déléguer ce travail au thread principal. Alors, les éléments graphiques seront bien modifiés par le thread principal et tout ira bien.

Commençons donc à analyser la mise en œuvre d'un Delegate(qui est plutôt folklorique) :

Delegate Sub dTest()

Private Sub Test()
   'Ma superbe fonction
End Sub

Nous avons donc deux parties dans un delegate :

  • 1. La déclaration du delegate ;

  • 2. La fonction à appeler.

La déclaration du delegate s'effectue comme une variable globale, une seule ligne commencée par le mot-clé Delegate et le nom du delegate.
Vient ensuite la fonction ou la méthode qui sera appelée, c'est à l'intérieur de celle-ci que seront modifiés les composants graphiques (un .text, un .enable, etc.).

Si vous avez des paramètres à passer à votre fonction, ce n'est pas vraiment plus compliqué :

Delegate Sub dTest(ByVal Texte1 As String, ByVal Texte2 As String)

Private Sub Test(ByVal Texte1 As String, ByVal Texte2 As String)
   'Ma superbe fonction avec arguments
End Sub

Il faut simplement recopier le prototype de votre fonction dans le delegate. Les noms des paramètres dans le delegate n'ont pas vraiment d'importance, mais pour éviter les erreurs je vous conseille de bien copier/coller ces derniers dans le delegate. Quoi qu'il en soit, les arguments doivent avoir le même ordre : si votre fonction appelle dans cet ordre : Boolean, String, Boolean, le delegate doit avoir aussi cet ordre : Boolean, String, Boolean.

Passons maintenant à l'étape qui doit vous intéresser : appeler le delegate.

Appel du delegate

On va voir un nouveau mot (j'ai l'impression de retourner en primaire :) ) : le Invoke.
Invoke va « invoquer » un delegate. On résume : on va invoquer un délégué pour faire notre travail à notre place (elle est pas belle la vie ?).

Me.Invoke(New dTest(AddressOf Test), Texte1, Texte2)

Alors, si votre delegate est sur la même classe, c'est un Me qui précède, sinon c'est le nom de la classe étrangère. Ensuite, vient la fonction Invoke. Elle a x paramètres ou x est le nombre d'arguments du delegate à invoquer + 1 (+1 car le premier est le delegate lui-même). Le premier argument est donc le delegate que l'on instancie (avec New) et l'on passe un paramètre à ce delegate, l'adresse de la fonction à appeler. Vous vous souvenez du mot-clé AddressOf ? Non ? Eh bien le revoilà ! Il suit ensuite les différents arguments de la fonction à invoquer, dans l'ordre aussi. :)

Compliqué ? On va résumer comme ça :

  • 1. J'écris la fonction qui va modifier les éléments graphiques.

  • 2. Je crée le delegate avec les même arguments que ma fonction, je lui donne le nom de ma fonction précédé de « d » pour me souvenir que c'est le delegate de cette dernière.

  • 3. J'invoque mon delegate en utilisant le mot-clé Invoke et en spécifiant le delegate à appeler, et dans ce delegate, la fonction à laquelle il fait référence.

Bon, j'admets que cette histoire de delegate est plutôt indigeste. Si vous ne comprenez pas bien leur utilisation, utilisez simplement l'exemple que j'ai donné plus haut en modifiant avec votre fonction et vos arguments. L'intellisense de Visual Studio est là pour vous aider aussi.

Prenez le temps de lire et relire cette partie sur les threads, ils vont être très importants lorsque nous ferons des programmes plus complexes en réseau.

  • Les threads permettent de désynchroniser l'exécution d'une fonction, ainsi si une exécution est bloquante, le programme principal ne sera pas bloqué.

  • On crée le thread en spécifiant l'adresse de la fonction qu'il exécutera.

  • La méthode Start permet de démarrer son exécution.

Exemple de certificat de réussite
Exemple de certificat de réussite