Les Threads. Sommaire. 1 Les Threads

Texte intégral

(1)

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   

 

(2)

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. 

             

(3)

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. 

                                 

(4)

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   

                     

(5)

2.1 Créer un Thread avec une méthode non­paramé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.  

                     

   

(6)

 

   

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

     

(7)

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. 

                                                   

(8)

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. 

                                   

(9)

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();

}

                                       

(10)

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. 

(11)

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. 

                                                         

(12)

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();

}

                                         

(13)

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();

}

           

(14)

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 

(15)

  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. 

         

(16)

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. 

             

(17)

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 : 

(18)

'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 

(19)

   

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 

(20)

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. 

                                                                             

(21)

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 

(22)

  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. 

                         

(23)

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 

(24)

  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! 

     

(25)

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 : 

(26)

'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 

(27)

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 

       

(28)

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

(29)

'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 

(30)

  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   

 

(31)

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 

(32)

   

//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();

}

           

(33)

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   

                                         

(34)

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 

(35)

  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 

(36)

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 

(37)

  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();

}

                         

(38)

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 : 

(39)

  '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 

(40)

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. 

 

(41)

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 Wait­Until­Done 

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 

Figure

Updating...

Sujets connexes :