Les Threads
Sommaire
Les Threads ... 1
1 Introduction ... 2
2 Les Threads : Notions de base ... 3
2.1 Créer un Thread avec une méthode non‐paramétrée ... 5
2.2 Les threads à méthode paramétrée ... 9
2.3 Gérer les arrêts des threads ... 10
2.4 Contexte d'exécution des threads ... 12
3 Gestion Multithread ... 14
3.1 Première solution : La classe Interlocked ... 17
3.2 Seconde solution : Verrouillage de bloc de code ... 21
3.3 Verrouillage avancé : La classe Monitor ... 23
3.4 Autres types de verrouillage ... 25
3.4.1 La classe ReaderWriterLock ... 25
3.4.2 La classe WaitHandle ... 28
3.5 Une erreur fréquente de Multithreading : Les Deadlocks ... 36
4 Les modèles de programmation asynchrone ... 38
4.1 Modèles de programmation "Rendezvous" ... 41
4.1.1 Le modèle Wait‐Until‐Done ... 41
4.1.2 Le modèle Polling ... 41
4.1.3 Le modèle Callback ... 43
4.2 L'APM et les exceptions ... 44
4.3 Premier outil des APM : La classe ThreadPool ... 45
4.3.1 Utilisation standard du pooling de threads ... 45
4.3.2 ThreadPool et WaitHandle ... 48
4.4 Un autre outil des APM : La classe SynchronizationContext ... 50
4.5 La classe Timer ... 51
5 Conclusion ... 53
1 Introduction
Jusqu'ici, toutes les opérations étaient effectuées sur un seul processus : Celui de notre application. Ce type de programmation est très limité. En effet, si nous lançons une tâche lourde (comme le traitement d'une vidéo ou le rendu d'une image par exemple), nous ne pouvons rien faire d'autre qu'attendre que le processus soit terminé.
Les Threads sont devenus indispensables aux développeurs souhaitant développer des applications performantes ; et cela est d'autant plus vrai depuis l’avènement des processeurs multi‐
cœurs sur les machines. Ils permettent d’effectuer des actions en parallèles dans une machine afin d’en accélérer le traitement.
Dans ce chapitre, nous allons voir comment le .NET Framework nous permet de créer des processus (ou Thread) en plus du processus principal. Nous verrons également quels sont les moyens de gestion des threads puis nous verrons le concept d'application asynchrone.
Tous les outils de gestion des processus se trouvent dans l'espace de nom System.Threading.
2 Les Threads : Notions de base
La classe Thread nous permet de créer simplement un objet qui va exécuter le contenu d'une méthode.
Les tableaux ci‐dessous en récapitulent les principaux membres et présentent également quelques énumérations utilisables avec cette classe :
¾ Membres d'instances
Propriétés Description
IsAlive Indique si le thread est en cours d’exécution ou non.
IsBackground Permet de savoir ou d’indiquer si un thread tourne en arrière plan.
IsThreadPoolThread Indique si le thread est dans le pool de threads.
ManagedThreadId Retourne un nombre permettant d’identifier le thread.
Name Permet de connaitre ou d’indiquer le nom du thread.
Priority Permet de connaitre ou d’indiquer une valeur de l’énumération ThreadPriority indiquant la priorité du thread.
ThreadState Retourne une des valeurs de l’énumération ThreadState reflétant l’état du thread.
Méthodes Description
Abort Arrête l’exécution du thread et lève une exception ThreadAbortException.
Interrupt Arrête l'exécution d'un thread dont l'état est WaitSleepJoin. Lève une exception ThreadInterruptedException.
Join Bloque le thread appelant jusqu’à ce que le thread ciblé se termine.
Start Lance l'exécution d'un thread
¾ Membres statiques
Propriétés Statiques Description
CurrentContext Retourne un objet ThreadContext basé sur le thread courant.
CurrentPrincipal Permet de connaitre ou d’indiquer l’utilisateur associé au thread courant.
CurrentThread Retourne le thread en cours d’exécution.
Méthodes Statiques Description
BeginCriticalRegion Entre ces deux méthodes, vous placez du code pouvant rendre le système instable. L’hôte sur lequel est hébergé le thread va décider quoi faire pour ne pas rendre l’application instable en cas
d’interruption du thread. Les régions critiques sont utiles lorsque plusieurs threads partagent des données.
EndCriticalRegion
GetDomain Retourne le domaine d'application associé au thread courant.
GetDomainID Retourne un numéro d’identification du domaine d'application associé au thread courant.
ResetAbort Annule une requête d'arrêt pour le thread courant.
Sleep Bloque le thread pendant un certain moment (en millisecondes). Passe la main aux autres threads.
SpinWait Bloque le thread pour un certain nombre d’itérations mais ne passe pas la main aux autres threads.
VolatileRead Lit la valeur d’un champ dans sa version la plus récente, quelque soit le processeur ayant écrit la valeur. (Environnement Multiprocesseur) VolatileWrite Ecrit une valeur dans un champ immédiatement de manière à ce
qu’elle soit accessible à tous les processeurs. (Environnement Multiprocesseur)
¾ Enumération ThreadState
Valeur Description
Aborted Le thread est interrompu
AbortRequested Le thread à reçu une demande d’interruption mais l’exception ThreadAbortException n’a pas encore été levée.
Background Le thread tourne en arrière plan.
Running Le thread est en cours d’exécution Stopped Le thread est arrêté.
StopRequested Le thread a reçu une demande d’arrêt
Unstarted Le thread a été crée mais n’a pas encore était démarré.
WaitSleepJoin Le thread est bloqué. (Par exemple par la méthode Join)
¾ Enumération ThreadPriority
Valeur Description
Highest Priorité la plus forte AboveNormal Entre Highest et Normal Normal Priorité par défaut BelowNormal Entre Normal et Lowest Lowest Priorité la moins forte
2.1 Créer un Thread avec une méthode nonparamétrée
Afin de créer un thread nous avons besoin d’une méthode qui sera exécutée par le thread. Elle ne doit rien retourner et ne prendre aucun paramètre. Il nous faut également un délégué qui va se charger d’exécuter la méthode au démarrage du thread et un objet thread afin de démarrer le thread:
Thread n° 11 ‐ Etat : Running
'VB
Public Sub affichage()
For i As Integer = 0 To 5 Step 1
Console.WriteLine("Thread n° {0} - Etat : {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.ThreadState) Thread.Sleep(1000)
Next End Sub Sub Main()
Dim delegue As ThreadStart = New ThreadStart(AddressOf affichage) Dim monThread As Thread = New Thread(delegue)
monThread.Start() Console.Read() End Sub
//C#
static void affichage() {
Console.WriteLine("Thread n° {0} - Etat : {1}",
Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.ThreadState);
}
static void Main(string[] args) {
ThreadStart delegue = new ThreadStart(affichage);
Thread monThread = new Thread(delegue);
monThread.Start();
Console.Read();
}
Nous avons donc crée la fonction affichage, qui affiche le numéro du thread courant et son état. Ensuite dans la fonction main, nous créons un délégué de type ThreadStart qui pointe sur notre fonction affichage. Enfin nous créons notre thread prenant notre délégué en paramètre. Il nous suffit alors de démarrer le thread pour pouvoir afficher ses informations.
Il faut bien comprendre que pendant que notre thread s’exécute, nous pouvons effectuer d’autres actions dans notre fonction main ; la fonction main étant elle‐même un processus.
//C#
static void affichage() {
for (int i = 0; i < 5; ++i) {
Console.WriteLine("Thread n° {0} - Etat : {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.ThreadState);
Thread.Sleep(1000);
} }
static void affichage2() {
Console.WriteLine("Second Thread est exécuté");
Thread.Sleep(2000);
Console.WriteLine("Second Thread se termine");
}
static void Main(string[] args) {
ThreadStart delegue = new ThreadStart(affichage);
Thread monThread = new Thread(delegue);
monThread.Start();
ThreadStart delegue2 = new ThreadStart(affichage2);
Thread monThread2 = new Thread(delegue2);
monThread2.Start();
Console.Read();
} 'VB
Public Sub affichage()
For i As Integer = 0 To 5 Step 1
Console.WriteLine("Thread n° {0} - Etat : {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.ThreadState) Thread.Sleep(1000)
Next End Sub
Public Sub affichage2()
Console.WriteLine("Second Thread est exécuté") Thread.Sleep(2000)
Console.WriteLine("Second Thread se termine") End Sub
Sub Main()
Dim delegue As ThreadStart = New ThreadStart(AddressOf affichage) Dim monThread As Thread = New Thread(delegue)
monThread.Start()
Dim delegue2 As ThreadStart = New ThreadStart(AddressOf affichage2) Dim monThread2 As Thread = New Thread(delegue2)
monThread2.Start()
Console.Read() End Sub
Nous avons donc crée deux threads. Chacun va effectuer un traitement différent, plus ou moins long et simulé par des pauses :
¾ Le premier effectue une boucle de cinq itérations où il affiche ses informations et fait une pause d’une seconde.
¾ Le second affiche qu’il débute, fait une pause de deux secondes puis affiche qu’il se termine.
Thread n° 10 ‐ Etat : Running Second Thread est exécuté Thread n° 10 ‐ Etat : Running Thread n° 10 ‐ Etat : Running Second Thread se termine Thread n° 10 ‐ Etat : Running Thread n° 10 ‐ Etat : Running
Si on regarde le résultat on voit que le premier thread démarre suivit immédiatement par le second. Puis, deux boucles du premier thread (2 secondes) sont passées et le second thread se termine. Le second thread a donc bien attendu 2 secondes avant de reprendre son déroulement, tout en laissant l’autre thread continuer d’itérer.
Il se peut que dans certains cas vous deviez attendre qu’un autre thread termine son traitement avant de continuer. Que ce soit le thread principal de votre fonction Main, ou un autre.
Pour cela on utilise la méthode Join.
La méthode Join va bloquer le thread dans lequel elle a été appelée, jusqu'à ce que le thread ciblé ait fini son exécution.
Reprenons le code précédent et rajoutons monThread.Join() :
Thread n° 10 ‐ Etat : Running
'VB
Sub Main()
Dim delegue As ThreadStart = New ThreadStart(AddressOf affichage) Dim monThread As Thread = New Thread(delegue)
monThread.Start() monThread.Join()
Dim delegue2 As ThreadStart = New ThreadStart(AddressOf affichage2) Dim monThread2 As Thread = New Thread(delegue2)
monThread2.Start() Console.Read() End Sub
//C#
static void Main(string[] args) {
ThreadStart delegue = new ThreadStart(affichage);
Thread monThread = new Thread(delegue);
monThread.Start();
monThread.Join();
ThreadStart delegue2 = new ThreadStart(affichage2);
Thread monThread2 = new Thread(delegue2);
monThread2.Start();
Console.Read();
}
Thread n° 10 ‐ Etat : Running Thread n° 10 ‐ Etat : Running Thread n° 10 ‐ Etat : Running Thread n° 10 ‐ Etat : Running Second Thread est exécuté Second Thread se termine
Cette fois‐ci, nous pouvons remarquer que le premier thread est lancé et stop le processus principal jusqu'à ce qu'il ait finit son exécution. Ainsi, le second thread n'est jamais démarré tant que le premier thread n'a pas réalisé ses 5 itérations.
2.2 Les threads à méthode paramétrée
Les threads que nous avons vus jusqu’à présent étaient simples et leurs méthodes exécutées ne comportaient pas de paramètres. Cependant, vous pouvez passer un paramètre à la fonction sur laquelle pointe le délégué. Pour cela, vous devez utiliser un délégué de type ParameterizedThreadStart au lieu de ThreadStart.
Une seule contrainte à cela : le paramètre doit être obligatoirement un objet de type Object :
Coucou 'VB
Public Sub affichage3(ByVal o As Object)
Console.WriteLine("{0}", CType(o, String)) End Sub
Sub Main()
Dim delegue_parametres As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf affichage3)
Dim monThread3 As Thread = New Thread(delegue_parametres) monThread3.Start("Coucou")
Console.Read() End Sub
//C#
static void affichage3(Object o) {
Console.WriteLine("{0}", (string)o);
}
static void Main(string[] args) {
ParameterizedThreadStart delegue_parametres = new ParameterizedThreadStart(affichage3);
Thread monThread3 = new Thread(delegue_parametres);
monThread3.Start("Coucou");
Console.Read();
}
2.3 Gérer les arrêts des threads
Pour arrêter un thread, il faut utiliser la méthode Abort. Celle‐ci va stopper le processus ciblé et va envoyer une exception ThreadAbortException . Cette exception peut ne pas être gérée par le code, cela n'interrompra pas le programme. Elle est juste levée pour signaler que le thread se termine.
//C#
static void maListe(Object o) {
List<string> liste = o as List<string>;
for (int i = 0; i < 10000000; i++) {
liste.Add("Test");
} }
static void Main(string[] args) {
List<string> liste = new List<string>();
ParameterizedThreadStart delegue_parametres2 = new ParameterizedThreadStart(maListe);
Thread monThread4 = new Thread(delegue_parametres2);
monThread4.Start(liste);
Console.WriteLine("{0}", liste.Count);
Console.WriteLine("{0}", liste.Count);
monThread4.Abort();
monThread4.Join();
Console.WriteLine("{0}", liste.Count);
Console.Read();
} 'VB
Public Sub maListe(ByVal o As Object)
Dim liste As List(Of String) = CType(o, List(Of String)) For i As Integer = 0 To 10000000 Step 1
liste.Add("Test") Next
End Sub Sub Main()
Dim liste As List(Of String) = New List(Of String)() Dim delegue_parametres2 As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf maListe)
Dim monThread4 As Thread = New Thread(delegue_parametres2) monThread4.Start(liste)
Console.WriteLine("{0}", liste.Count) Console.WriteLine("{0}", liste.Count) monThread4.Abort()
monThread4.Join()
Console.WriteLine("{0}", liste.Count) End Sub
Dans ce code, nous chargeons notre thread de rajouter dans une liste dix millions de fois le mot "Test" et nous affichons périodiquement la taille de la liste afin de voir où en est le traitement.
Au final nous utilisons la méthode Join pour que le thread termine proprement son travail.
Nous exécutons d’abord notre code en mettant en commentaire la ligne monThread4.Abort(); :
0 585261 10000000
…puis, nous dé‐commentons ensuite la ligne:
0 1384534 1572210
Comme vous pouvez le voir, le thread a été interrompu en cours de traitement car il n’a pas pu terminer la boucle dans laquelle il était. La méthode Join ne sert plus à grand‐chose.
Il faut faire attention à la méthode Abort. En effet, celle‐ci peut arrêter le thread à n’importe quel moment du thread et dans certains cas, rendre l’application instable. Dans le cas d’applications complexes ou plusieurs threads partagent les mêmes données, il faudra utiliser les régions critiques afin que l’application ne devienne pas instable.
Pour cela, vous déclarez une région critique de code avec la méthode BeginCriticalRegion. Ensuite vous placez le code que vous estimez critique et vous terminez la région avec la méthode EndCriticalRegion. Je vous invite à lire à nouveau le tableau des méthodes statiques de la partie 2 pour avoir une description des zones critiques.
2.4 Contexte d'exécution des threads
Tout comme pour la sérialisation, chaque thread possède un contexte d’exécution propre, définissant par exemple des informations de sécurités. Vous pouvez manipuler le flux contenant le contexte en utilisant les membres statiques de la classe ExecutionContext.
Le contexte d’exécution va vous permettre d’échanger des données entre threads de manière sécurisée et intègre.
Si vous souhaitez utiliser des threads sans contexte particulier, vous devrez utiliser la méthode SuppressFlow. Stopper le flux permet de gagner en performances, en contre partie nous perdons les informations de sécurité, de culture et le contexte de transaction.
Ici nous avons supprimé le flux et nous lançons notre Thread normalement. Remarquez que nous avons imbriqué le délégué directement en paramètre de Thread.
'VB
Sub Main()
Dim flow As AsyncFlowControl = ExecutionContext.SuppressFlow() Dim monThread5 As Thread = New Thread(New ThreadStart(AddressOf affichage2))
monThread5.Start() monThread5.Join()
Console.Read() End Sub
//C#
static void Main(string[] args) {
AsyncFlowControl flow = ExecutionContext.SuppressFlow();
Thread monThread5 = new Thread(new ThreadStart(affichage2));
monThread5.Start();
monThread5.Join();
Console.Read();
}
Si vous voulez restaurez finalement le contexte, vous pouvez utiliser RestoreFlow ou la méthode Undo de l'objet AsyncFlowControl (ici, l'objet flow):
Une fois que notre thread à terminé, nous restaurons le contexte d'exécution pour les autres threads.
Vous pouvez enfin utiliser la méthode Run afin de créer et modifier le contexte d'exécution:
Nous récupérons le contexte actuel avec la méthode Capture de ExecutionContext, puis avec la méthode Run nous passons en premier paramètre la référence au nouveau contexte, en second paramètre un délégué pointant sur une méthode ContexteAppele à exécuter dans le contexte fourni, et un l'objet qui sera passé en paramètre de la méthode exécutée (ici, aucun objet est utilisé). La méthode ContexteAppele est une simple fonction qui ne retourne rien et prend une variable de type Object en unique paramètre.
'VB
Sub Main()
Dim contexte As ExecutionContext = ExecutionContext.Capture() ExecutionContext.Run(contexte, New ContextCallback(AddressOf ContexteAppele), Nothing)
End Sub
//C#
static void Main(string[] args) {
ExecutionContext contexte = ExecutionContext.Capture();
ExecutionContext.Run(contexte, new ContextCallback(ContexteAppele), null);
} 'VB
Sub Main()
Dim flow As AsyncFlowControl = ExecutionContext.SuppressFlow() Dim monThread5 As Thread = New Thread(New ThreadStart(AddressOf affichage2))
monThread5.Start() monThread5.Join()
ExecutionContext.RestoreFlow() Console.Read()
End Sub
//C#
static void Main(string[] args) {
AsyncFlowControl flow = ExecutionContext.SuppressFlow();
Thread monThread5 = new Thread(new ThreadStart(affichage2));
monThread5.Start();
monThread5.Join();
ExecutionContext.RestoreFlow();
Console.Read();
}
3 Gestion Multithread
Dans un environnement multithread, nous pouvons arriver dans le cas où plusieurs Threads cherchent à accéder aux mêmes données.
Considérons le code suivant :
'VB
Dim threada, threadb As Thread
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object)
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
CType(o, Value).value += 1 Next
Thread.CurrentThread.Abort(Nothing) End Sub
Sub Main()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Si nous interprétons ce code, nous créons un objet nommé "v" contenant une valeur unique.
Nous créons également deux threads ainsi qu'une seule méthode à exécuter. Dans cette méthode, nous incrémentons la valeur contenue dans notre objet.
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static void methode(Object o) {
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++) ((Value)o).value += 1;
Thread.CurrentThread.Abort(null);
}
static void Main(string[] args) {
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
Or à l'exécution du programme : ID : 10 dispose de 0
ID : 11 dispose de 0 1628698
On remarque que, suite à l'exécution des deux threads le résultat est différent de 2000000 comme on aurait pu le prévoir.
Cela est dû au fait que les processeurs actuels réalisent les opérations en plusieurs étapes. Si on décrit grossièrement les étapes, ils chargent la donnée à modifier dans leur mémoire interne (mémoire cache), puis ils effectuent l'opération désirée et enfin, ils stockent dans la mémoire vive la valeur issue du calcul.
Seulement, ces mêmes processeurs sont en général équipés de circuits de traitements permettant à plusieurs processus de s'exécuter (a quelques intervalles près) en même temps.
Le schéma ci‐dessous reprend ce principe d'exécution simultanée :
Dans ce schéma, chacun de nos threads est représenté par une couleur. Cette couleur est réutilisée dans la colonne de droite pour spécifier quel thread a modifié la valeur de V en mémoire.
Dans le détail de l'exécution du processeur :
¾ Les cases rouges représentent l'opération de copie de la donnée à modifier (de la mémoire vive vers la mémoire cache du CPU).
¾ Les cases vertes représentent l'opération à effectuer (ici une incrémentation).
¾ Les cases bleues représentent l'opération inverse aux cases rouge (copie de la donnée de la mémoire cache du CPU vers la mémoire vive)
Dans tous les cas, une référence de temps est présentée par "T0", "T1", … .
Au final, ce schéma montre bien que, même si les deux threads se sont exécutés, la valeur n'aura été incrémentée que de 1 alors qu'on aurait voulu qu'elle soit incrémentée de 2.
Note : Ce procédé d'exécution simultanée est réalisé grâce au principe de Pipelining (comprenez "ligne d'exécution"). Si vous souhaitez en apprendre d'avantage sur le Multithread et le pipelining, je vous recommande ce premier lien ainsi que celui sur les pipelines.
Et ça ne s'arrête pas là. En effet, en plus du problème de multithreading, les ordinateurs actuels disposent de techniques permettant à plusieurs threads de fonctionner sur une seule ligne d'exécution (En donnant un temps d'accès CPU limité à chaque thread) : Le Multithreading simultané.
Cela veut dire que, par exemple, le ThreadA pourrait être interrompu par l'hôte avant qu'il n'ait eu le temps de copier la valeur de la mémoire cache vers la mémoire vive, laissant ainsi au ThreadB la valeur initiale de "v" à savoir 0.
3.1 Première solution : La classe Interlocked
Cette classe donne accès à des méthodes statiques permettant d'indiquer au système d'exploitation qu'une donnée ne peut pas être modifiée par deux threads en même temps, tant que le thread qui a récupéré la donnée n'a pas fini son opération. Elles interdisent également à l'hôte d'interrompre un processus prématurément afin de donner la main à un autre processus.
Voici les principales méthodes de cette classe :
Méthode Description
Add Ajoute deux entiers entre eux et remplace le premier entier par le résultat, le tout en une opération atomique.
Decrement Décrémente de 1 la valeur passée en paramètre en réalisant une opération atomique.
Exchange Echange le contenu de deux objets en effectuant une opération atomique.
Increment Incrémente de 1 la valeur passée en paramètre en réalisant une opération atomique.
Read Retourne un entier codé sur 64bits en effectuant une opération atomique.
Note : Une opération atomique est une opération qui ne peut pas être interrompue.
Ainsi, nous pourrions remplacer la ligne d'incrémentation de la valeur par la méthode Increment :
'VB
Dim threada, threadb As Thread
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object)
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) Next
Thread.CurrentThread.Abort(Nothing) End Sub
Sub Main()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Si nous reprenons le schéma du dessus en le modifiant :
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static void methode(Object o) {
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++)
Interlocked.Increment(ref ((Value) o).value);
Thread.CurrentThread.Abort(null);
}
static void Main(string[] args) {
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
Ainsi, le schéma nous montre clairement que cette fois‐ci, le ThreadB doit attendre que le
Note : Ne pensez pas que ce procédé vous fait perdre des performances. En effet, les processeurs possèdent bien plus de deux lignes d'exécution et les systèmes d'exploitation s'arrangent pour que chaque ligne soit occupée dans le bon ordre. Ainsi, si notre ThreadB se trouve sur la troisième ligne d'exécution au lieu de la seconde, le système d'exploitation va en fait occuper la seconde ligne d'exécution avec un autre Thread.
Peu importe votre type de processeur, le résultat restera cette fois‐ci toujours le même : ID : 10 dispose de 0
ID : 11 dispose de 0 2000000
Vous pouvez également constater que le problème n'est que partiellement résolu. En effet, au démarrage des deux threads, ceux‐ci ont encore pour valeur initiale le 0 de départ de notre valeur. Le problème se résout uniquement lorsque l'un des deux threads à finit d'incrémenter la valeur.
3.2 Seconde solution : Verrouillage de bloc de code
La classe Interlocked est utile si vous n'effectuez que des petits traitements. Elle devient dépassée si vous travaillez sur des objets plus complexes. Aussi, le .NET Framework propose une autre solution pour verrouiller les données : Les mots clés lock en C# et SyncLock en VB.NET.
Si nous reprenons le code précédent en apportant les modifications nécessaire :
'VB
Dim threada, threadb As Thread
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object) SyncLock o
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
CType(o, Value).value += 1 Next
Thread.CurrentThread.Abort(Nothing) End SyncLock
End Sub
Sub Main()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Nous obtenons à la compilation :
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static void methode(Object o) {
lock (o) {
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++) ((Value)o).value += 1;
Thread.CurrentThread.Abort(null);
} }
static void Main(string[] args) {
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
ID : 11 dispose de 0 ID : 10 dispose de 1000000 2000000
A l'image de Interlocked, la valeur utilisée est donc verrouillée sauf que cette fois‐ci, elle reste verrouillée tant que tout le code de la méthode à exécuter n'a pas été exécuté. Cela se remarque car le premier thread dispose de la valeur initiale 0 alors que le second thread démarre à 1000000.
Grâce à ces mots clés, nous ne verrouillons plus de valeur unique mais des portions de code.
3.3 Verrouillage avancé : La classe Monitor
La classe Monitor propose un ensemble de méthodes statiques pour verrouiller des portions de code de façon un peu plus construite.
Voici les principales méthodes :
Méthode Description
Enter Fourni un verrouillage sur l'objet passé en paramètre.
TryEnter Tente de créer un verrouillage sur l'objet passé en paramètre. Peut également fournir un verrouillage avec une limite de temps.
Exit Déverrouille l'objet passé en paramètre
Wait Déverrouille l'objet passé en paramètre et endort le thread jusqu'à ce qu'il puisse a nouveau obtenir un verrouillage sur l'objet.
Si nous reprenons le code précédent :
'VB
Dim threada, threadb As Thread
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object) Monitor.Enter(o)
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) If (i = 1) Then
Monitor.Exit(o) End If
Next
Thread.CurrentThread.Abort(Nothing) End Sub
Sub Main()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
A l'exécution, nous devrions obtenir un résultat similaire à celui‐ci :
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static void methode(Object o) {
Monitor.Enter(o);
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++) {
Interlocked.Increment(ref ((Value) o).value);
if(i == 1)
Monitor.Exit(o);
}
Thread.CurrentThread.Abort(null);
}
static void Main(string[] args) {
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
ID : 10 dispose de 0 ID : 11 dispose de 750 2000000
Le ThreadA démarre et verrouille l'objet au début de son exécution. Ensuite, le ThreadB démarre à son tour et voit son exécution bloquée tant que le ThreadA n'a pas atteint la ligne
"Monitor.exit(o)". Pour l'incrémentation, nous utilisons toujours Interlocked car une fois que le ThreadA a débloqué l'objet, les deux threads peuvent de nouveau accéder à la même donnée.
Le ThreadB commence avec une valeur différente de 1 tout simplement car le temps qu'il sorte de sa mise en veille, le ThreadA à eu le temps d'effectuer plusieurs incrémentations atomiques verrouillant l'objet à incrémenter.
Note : Si votre processeur est plus puissant que celui qui a été utilisé pour cet exemple, il est fort possible que vous obteniez une valeur de démarrage du ThreadB plus élevée. Il est également possible que la valeur de démarrage utilisée par le ThreadB soit de 1 si vous avez beaucoup de chance!
3.4 Autres types de verrouillage
3.4.1 La classe ReaderWriterLock
Cette classe permet de gérer deux types de verrouillage : Les verrouillages en écriture et les verrouillages en lecture.
Tout objet verrouillé en lecture par un thread, devra être déverrouillé avant qu'un autre thread, cherchant à verrouiller le même objet en écriture, puisse obtenir son verrouillage. Il est tout à fait possible que plusieurs threads différents puissent obtenir un verrouillage en lecture sur un seul et même objet. Il faut également retenir que s'il est possible d'obtenir plusieurs verrouillages en lecture, il est impossible que plusieurs threads obtiennent un verrouillage en écriture.
En voici les principales propriétés (en vert) et méthodes(en bleu) de cette classe :
Membres Description
IsReaderLockHeld Indique si un verrou de lecture est actuellement en place.
IsWriterLockHeld Indique si un verrou d'écriture est actuellement en place.
AcquireReaderLock Tente d'obtenir un verrouillage de lecture pendant un temps donné. Si au bout du temps spécifié, aucun verrou n'a été créé, une exception sera levée.
AcquireWriterLock Identique à AcquireReaderLock mais pour placer un verrou en écriture.
DowngradeFromWriterLock Convertit un verrou d'écriture en un verrou de lecture.
ReleaseReaderLock Supprime un verrou en lecture d'un objet.
ReleaseWriterLock Supprime un verrou en écriture d'un objet.
UpgradeToWriterLock Convertit un verrou de lecture en un verrou d'écriture.
En reprenant notre code, l'exemple ci‐dessous tente de verrouiller nos deux threads en écriture. Si le premier verrouillage est un succès, le second échouera :
'VB
Dim threada, threadb As Thread Dim locker As ReaderWriterLock Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object) Try
locker.AcquireWriterLock(1)
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) If (i = 1) Then
locker.ReleaseWriterLock() End If
Next
Catch ex As Exception
Console.WriteLine("Verrouillage du thread {0} en écriture impossible", Thread.CurrentThread.ManagedThreadId)
End Try
Thread.CurrentThread.Abort(Nothing) End Sub
Sub Main()
locker = New ReaderWriterLock() Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
ID : 11 dispose de 0 //C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static ReaderWriterLock locker;
public static void methode(Object o) {
try {
locker.AcquireWriterLock(1);
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++) {
Interlocked.Increment(ref ((Value) o).value);
if(i == 1)
locker.ReleaseWriterLock();
} }
catch(Exception ex) {
Console.WriteLine("Verrouillage du thread {0} en écriture impossible", Thread.CurrentThread.ManagedThreadId);
}
Thread.CurrentThread.Abort(null);
}
static void Main(string[] args) {
locker = new ReaderWriterLock();
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
Verrouillage du thread 12 en écriture impossible 1000000
3.4.2 La classe WaitHandle
Cette classe abstraite fourni aux classes qui en hérite un système permettant de spécifier le blocage ou la libération de ressources partagées.
Voici les principaux membres :
Membres Description
Handle Obtient ou définit un pointeur vers les ressources à verrouiller.
Important : Cette propriété est dépréciée, utilisez la propriété SafeWaitHandle à la place.
Close Permet de libérer l'objet verrouillé.
WaitOne Bloque le thread courant jusqu'à ce que l'objet Handle reçoive un signal de libération.
Le .NET Framework propose de base quelques classes qui héritent de WaitHandle.
3.4.2.1 La classe Mutex
Cette classe fonctionne de la même façon que la classe Monitor. Son avantage est qu'elle peut également fournir des verrous sur des ressources partagées en dehors du domaine d'application.
Elle propose, en plus des méthodes de sa classe parente, la méthode ReleaseMutex afin de supprimer le verrou de la ressource. Elle dispose également de la méthode statique OpenExisting qui permet de charger un objet Mutex créé en dehors du domaine d'application courant et nommé via sa propriété Name.
'VB
Dim threada, threadb As Thread Dim locker As Mutex
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object) Try
If (locker.WaitOne(1, False)) Then
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) Next
locker.ReleaseMutex()
Thread.CurrentThread.Abort(Nothing) Else
Console.WriteLine("Thread {0} mis en attente", Thread.CurrentThread.ManagedThreadId)
End If
Catch ex As Exception
Console.WriteLine(ex.Message) End Try
End Sub Sub Main()
locker = New Mutex()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Ce code va fourni un verrou sur le premier thread à accéder à la méthode d'exécution.
Seulement, lorsque le second thread va tenter d'obtenir un verrou, il sera mis en attente un temps trop court (1 milliseconde) pour qu'il puisse être exécuté.
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static Mutex locker;
public static void methode(Object o) {
try {
if(locker.WaitOne(1, false)) {
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++)
Interlocked.Increment(ref ((Value) o).value);
locker.ReleaseMutex();
Thread.CurrentThread.Abort(null);
} else
Console.WriteLine("Thread {0} mis en attente", Thread.CurrentThread.ManagedThreadId);
}
catch(Exception ex) {
Console.WriteLine(ex.Message);
} }
static void Main(string[] args) {
locker = new Mutex();
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
ID : 12 dispose de 0 Thread 13 mis en attente Le thread a été abandonné.
1000000
3.4.2.2 La classe Semaphore
Cette classe permet de bloquer une ressource après un certain nombre d'accès. C'est un peu l'équivalent des "slots" sur les serveurs de jeu : La classe Semaphore couplée à une ressource constitue le serveur et les threads représentent les joueurs. Dès qu'il n'y a plus de place disponible sur le serveur, les joueurs sont éjectés ce qui est équivalent à dire que dès que la classe Semaphore n'a plus de place disponible, les threads tentant d'accéder à la ressource seront bloqués jusqu'à ce que la ressource soit libérée.
Tout comme Mutex, elle peut être nommée pour être chargée dans un autre domaine d'application.
'VB
Dim threada, threadb As Thread Dim locker As Semaphore
Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object)
While (Thread.CurrentThread.IsAlive) Try
If (locker.WaitOne(1, False)) Then
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) Next
locker.Release(1)
Thread.CurrentThread.Abort(Nothing) Else
Console.WriteLine("Thread {0} mis en attente", Thread.CurrentThread.ManagedThreadId)
End If
Catch ex As Exception
Console.WriteLine(ex.Message) End Try
End While End Sub
Sub Main()
locker = New Semaphore(1,1) Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static Semaphore locker;
public static void methode(Object o) {
while(Thread.CurrentThread.IsAlive) {
try {
if(locker.WaitOne(1, false)) {
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++)
Interlocked.Increment(ref ((Value) o).value);
locker.Release(1);
Thread.CurrentThread.Abort(null);
} else
Console.WriteLine("Thread {0} mis en attente", Thread.CurrentThread.ManagedThreadId);
}
catch(Exception ex) {
Console.WriteLine(ex.Message);
} } }
static void Main(string[] args) {
locker = new Semaphore(1,1);
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
Ce code n'autorise qu'un seul verrouillage à la fois et par un seul thread. Autrement dit, le second thread devra attendre que le premier libère la ressource avant de pouvoir se lancer.
ID : 12 dispose de 0 Thread 13 mis en attente Thread 13 mis en attente ID : 13 dispose de 1000000 Le thread a été abandonné.
Le thread a été abandonné.
2000000
3.4.2.3 Les verrouillages évènementiels
Les classes AutoResetEvent et ManualResetEvent héritent toutes les deux de EventWaitHandle qui hérite elle‐même de WaitHandle.
La classe EventWaitHandle fourni deux méthodes Set et Reset permettant respectivement d'activer et de désactiver le verrouillage des threads.
En gros, ces classes permettent juste d'activer ou non l'accès à des ressources. La différence entre les deux est que AutoResetEvent va retourner automatiquement à son état de verrou actif après désactivation du verrou.
Tout comme les deux autres objets de verrouillage Semaphore et Mutex, ils peuvent également être nommés pour être chargés en dehors du domaine d'application.
'VB
Dim threada, threadb As Thread Dim locker As AutoResetEvent Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object)
While (Thread.CurrentThread.IsAlive)
If (locker.WaitOne(500, False)) Then
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) Next
locker.Set()
Thread.CurrentThread.Abort(Nothing) Else
Console.WriteLine("Le thread {0} a été endormi!", Thread.CurrentThread.ManagedThreadId)
End If End While End Sub
Sub Main()
locker = New AutoResetEvent(False) Dim v As Value = New Value()
v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v) Thread.Sleep(2000) locker.Set()
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Ce code va bloquer l'exécution des deux processus pendant 2 secondes. Ces deux processus seront endormis pendant 500ms avant de re‐tester s'ils peuvent accéder aux ressources.
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static AutoResetEvent locker;
public static void methode(Object o) {
while(Thread.CurrentThread.IsAlive) {
if(locker.WaitOne(500, false)) {
Console.WriteLine("ID : {0} dispose de {1}", Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for (int i = 0; i < 1000000; i++)
Interlocked.Increment(ref ((Value) o).value);
locker.Set();
Thread.CurrentThread.Abort(null);
} else
Console.WriteLine("Le thread {0} a été endormi!", Thread.CurrentThread.ManagedThreadId);
} }
static void Main(string[] args) {
locker = new AutoResetEvent(false);
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
Thread.Sleep(2000);
locker.Set();
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
Le thread 12 a été endormi!
Le thread 11 a été endormi!
Le thread 11 a été endormi!
Le thread 12 a été endormi!
Le thread 11 a été endormi!
Le thread 12 a été endormi!
ID : 11 dispose de 0
Le thread 12 a été endormi!
ID : 12 dispose de 1000000 2000000
3.5 Une erreur fréquente de Multithreading : Les Deadlocks
A l'image des fameux BSoD (Blue Screen of Death) de Windows, le développement d'application multithread met en avant les deadlocks.
Traduit littéralement, cela voudrait dire "Verrouillage mort". Les deadlocks, c'est le fait de faire verrouiller un ou plusieurs objets par plusieurs threads sans jamais leur donner la possibilité de déverrouiller l'objet. Le code se trouve dans un cas d'exécution bloquée où les threads ne peuvent plus rien faire car ils verrouillent tous l'objet, empêchant ainsi les autres threads de s'exécuter et réciproquement.
'VB
Dim threada, threadb As Thread Public Class Value
Public value As Integer End Class
Sub methode(ByVal o As Object) Monitor.Enter(o)
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, CType(o, Value).value) For i As Integer = 1 To 1000000
Interlocked.Increment(CType(o, Value).value) If (i = 1) Then
Monitor.Exit(o) End If
Next
Thread.CurrentThread.Abort(Nothing) End Sub
Sub Main()
Dim v As Value = New Value() v.value = 0
Dim operation As ParameterizedThreadStart = New ParameterizedThreadStart(AddressOf methode)
threada = New Thread(operation) threadb = New Thread(operation) threada.Start(v)
threadb.Start(v) Monitor.Enter(v)
threada.Join() threadb.Join()
Console.WriteLine(v.value) Console.Read()
End Sub
Si nous compilons ce programme, l'exécution de celui‐ci se bloque au début de l'exécution du ThreadA. En effet, dès que les deux threads ont démarré, le thread principal de l'application verrouille l'objet puis ce même thread attend que les deux autres threads aient fini leur tache ; tache qui ne peut plus avancer puisque l'objet sur lequel ils travaillent, est verrouillé par le processus principal : L'application est "plantée".
//C#
public class Value {
public int value;
}
public static Thread threada, threadb;
public static void methode(Object o) {
Monitor.Enter(o);
Console.WriteLine("ID : {0} dispose de {1}",
Thread.CurrentThread.ManagedThreadId, ((Value)o).value);
for(int i = 0; i < 1000000; i++) {
Interlocked.Increment(ref ((Value) o).value);
if(i == 1)
Monitor.Exit(o);
}
Thread.CurrentThread.Abort(null);
}
static void Main(string[] args) {
Value v = new Value();
v.value = 0;
ParameterizedThreadStart operation = new ParameterizedThreadStart(methode);
threada = new Thread(operation);
threadb = new Thread(operation);
threada.Start(v);
threadb.Start(v);
Monitor.Enter(v);
threada.Join();
threadb.Join();
Console.WriteLine(v.value);
Console.Read();
}
4 Les modèles de programmation asynchrone
Le modèle de programmation asynchrone (APM pour Asynchronous Programming Model) permet d’effectuer des traitements dans un thread sans pour autant bloquer les autres threads. En effet jusqu’à présent, nous utilisions les threads afin d’effectuer des opérations en parallèle, mais en fait, chaque Thread se contentait de bloquer les autres processus pour effectuer son traitement puis passait la main au thread suivant.
On utilise l’APM le plus souvent pour les traitements lourds ou longs (ouverture de fichiers, connexion distante (à une base de données ou une autre machine)), afin de pouvoir continuer à faire tourner l’application pendant que la méthode asynchrone s’exécute.
Cela va nous permettre par exemple dans le cas d’un jeu vidéo en ligne de rechercher un serveur tout en continuant le programme principal (gestion de la musique, des évènements, de l’affichage…).
Dans le Framework .NET, de nombreuses classes supportent l’APM grâce à des méthodes particulières. Souvent ces méthodes ressemblent à leur version non asynchrone et sont précédées des mots Begin ou End (par exemple, la classe FileStream possède un équivalent asynchrone à la méthode Read : BeginRead et EndRead). Vous pouvez aussi rendre asynchrone vos propres méthodes en utilisant les méthodes BeginInvoke et EndInvoke associées au délégué pointant vers la méthode à exécuter.
Pour mieux comprendre nous allons utiliser BeginInvoke et EndInvoke. Sachez que si vous souhaitez utiliser une méthode asynchrone déjà implémentée, le code sera sensiblement identique.
BeginInvoke et EndInvoke sont des méthodes d’un délégué asynchrone dont la signature est la même que la méthode que vous allez appeler. Quand vous avez crée et instancié votre délégué, vous allez pouvoir initier l’appel de votre méthode de manière asynchrone grâce à BeginInvoke.
La méthode BeginInvoke utilise les mêmes paramètres que la méthode appelée suivit de deux paramètres optionnels :
¾ Un délégué AsyncCallback qui pointe sur une méthode à appeler lorsque l’appel asynchrone se termine.
¾ Un objet qui vous permet de passer des données à la méthode exécutée par le délégué.
La méthode EndInvoke permet de fermer l’appel à la méthode. Veillez à bien l’appeler dans vos programmes. Si vous utilisez EndInvoke avant que l’appel à la méthode soit terminé, EndInvoke va bloquer les autres threads jusqu’à que l’appel se termine.
Voici un exemple d’utilisation de BeginInvoke et EndInvoke :
'VB
Public Delegate Function AsyncAppelMethode(ByVal tempsAttente As Integer, ByRef idThread As Integer) As String
Public Class AsyncAffichage
Public Function maMethode(ByVal tempsAttente As Integer, ByRef idThread As Integer) As String
Console.WriteLine("Début de la méthode, veuillez patienter") Thread.Sleep(tempsAttente)
idThread = Thread.CurrentThread.ManagedThreadId
Return String.Format("Cet affichage a eu lieu après une attente de {0} millieme de secondes", tempsAttente.ToString()) End Function
End Class Sub Main()
Dim idThread As Integer Dim a As Integer = 0
Dim affichage As AsyncAffichage = New AsyncAffichage()
Dim appel As AsyncAppelMethode = New AsyncAppelMethode(AddressOf affichage.maMethode)
Dim resultat As IAsyncResult = appel.BeginInvoke(10000, idThread, Nothing, Nothing)
For i As Integer = 1 To 1000000000 Step 1 a += 1
Next
Console.WriteLine("{0}", a)
Console.WriteLine("Le thread principal n'est pas bloqué pendant l'appel de la méthode")
Dim valeurDeRetour As String = appel.EndInvoke(idThread, resultat) Console.WriteLine("L'appel a été effectué par le thread {0} et a retourné : {1}", idThread, valeurDeRetour)
Console.Read() End Sub
Début de la méthode, veuillez patienter //C#
public delegate string AsyncAppelMethode(int tempsAttente, out int idThread);
public class AsyncAffichage {
public string maMethode(int tempsAttente, out int idThread) {
Console.WriteLine("Début de la méthode, veuillez patienter");
Thread.Sleep(tempsAttente);
idThread = Thread.CurrentThread.ManagedThreadId;
return String.Format("Cet affichage a eu lieu après une attente de {0} millieme de secondes", tempsAttente.ToString());
} }
static void Main(string[] args) {
int idThread;
int a = 0;
AsyncAffichage affichage = new AsyncAffichage();
AsyncAppelMethode appel = new AsyncAppelMethode(affichage.maMethode);
IAsyncResult resultat = appel.BeginInvoke(10000, out idThread, null, null);
for (int i = 0; i < 1000000000; i++) a++;
Console.WriteLine("{0}", a);
Console.WriteLine("Le thread principal n'est pas bloqué pendant l'appel de la méthode");
string valeurDeRetour = appel.EndInvoke(out idThread, resultat);
Console.WriteLine("L'appel a été effectué par le thread {0} et a retourné : {1}", idThread, valeurDeRetour);
Console.Read();
}
1000000000
Le thread principal n'est pas bloqué pendant l'appel de la méthode
L'appel a été effectué par le thread 7 et a retourné : Cet affichage a eu lieu après une attente de 10000 millième de secondes
Nous avons donc créé un délégué retournant un string et possédant deux paramètres, un entier, et un entier passé par référence.
Dans la classe AsyncAffichage nous avons une méthode ayant la même signature que le délégué. Cette méthode est simple, elle affiche une chaine, fait patienter le thread un certain temps, puis attribut une valeur à idThread et retourne une chaine.
Dans notre Main, nous instancions notre classe AsyncAffichage, et notre délégué AsyncAppelMethode.
Ensuite nous récupérons le retour de la méthode BeginInvoke de notre délégué dans une variable résultat de type IAsyncResult. BeginInvoke prend en paramètre les deux paramètres de la méthode d’AsyncAffichage suivit de deux paramètres mit à null car facultatif ici.
Nous effectuons ensuite une boucle conséquente pour montrer que le thread principal continue de tourner.
Enfin nous récupérons le retour dans la méthode EndInvoke qui est en fait le retour de la méthode d’AsyncAffichage. EndInvoke prend en paramètre toutes les valeurs passées par référence au délégué, donc ici idThread, puis le retour de BeginInvoke.
Enfin nous affichons le retour de la méthode d’AsyncAffichage.
Le problème de ce code est que si nous déplaçons EndInvoke au dessus de notre imposante boucle for, celle‐ci sera bloquée jusqu’à que l’appel soit terminé, ce qui devient bien moins intéressant. Nous allons donc devoir utiliser un autre concept de l’APM : Le modèle « Rendez Vous ».
4.1 Modèles de programmation "Rendezvous"
Les modèles de programmation "rendez‐vous" sont contenus dans 3 modèles de programmation asynchrone. Parmi les trois techniques, sachez que nous utilisons le plus souvent la dernière :
¾ Le modèle « Wait‐Until‐Done »
¾ Le modèle « Polling »
¾ Le modèle « Callback »
4.1.1 Le modèle WaitUntilDone
Le modèle Wait‐Until‐Done est celui que nous avons utilisé dans notre exemple précédent.
Cela consiste simplement à appeler la méthode EndInvoke qui va bloquer les threads et d’attendre que l’appel se termine avant de continuer. Ce modèle est peu utile, il revient à utiliser les threads de la même manière que dans la partie 1. N’utilisez pas cette méthode si vos méthodes effectuent de lourds traitements sous peine de voir votre application se figer.
4.1.2 Le modèle Polling
Le modèle Polling ressemble au modèle Wait‐Until‐Done, à la différence qu’il va effectuer une boucle vérifiant sans arrêt si l’appel est terminé avant d’appeler EndInvoke. Voici un exemple d’utilisation (nous ne présentons ici que la partie qui change pour plus de clarté) :
'VB
Sub Main()
Dim idThread As Integer Dim a As Integer = 0
Dim affichage As AsyncAffichage = New AsyncAffichage()
Dim appel As AsyncAppelMethode = New AsyncAppelMethode(AddressOf affichage.maMethode)
Dim resultat As IAsyncResult = appel.BeginInvoke(10000, idThread, Nothing, Nothing)
For i As Integer = 1 To 1000000000 Step 1 a += 1
Next
Console.WriteLine("{0}", a)
Console.WriteLine("Le thread principal n'est pas bloqué pendant l'appel de la méthode")
While (Not resultat.IsCompleted)
Console.WriteLine("Divers traitements") Thread.Sleep(5000)
End While
Dim valeurDeRetour As String = appel.EndInvoke(idThread, resultat) Console.WriteLine("L'appel a été effectué par le thread {0} et a retourné : {1}", idThread, valeurDeRetour)
Console.Read() End Sub