Un peu d'histoire
On parle beaucoup de threads ces temps-ci. Les nouveaux processeurs sont des puces conçues afin d'optimiser le traitement de plusieurs threads simultanés. Il y a quelques années, à l'ère du Pentium 4, on ne cessait d'augmenter la fréquence d'horloge afin d'optimiser la vitesse d'un seul coeur. Chaque thread avait un numéro et attendait son tour afin d'être traité.
Ne vous méprenez pas, cela n'a pas changé, mais les processeurs ont commencé à les traiter simultanément, d'abord avec la technologie HT sur les derniers Pentium 4, puis à l'aide de multiples coeurs avec les Core 2 Duo / Quad. Maintenant, il s'agit d'un mélange des deux, soit de multiples coeurs qui appliquent chacun une technologie HT, comme dans le Core i7 d'Intel. Pardonnez-moi, mais je connais très mal les processeurs AMD, étant un utilisateur d'Intel majoritairement

.
Cela explique un peu l'évolution de la technique de traitement des threads, mais je ne vous ai toujours pas expliqué comment fonctionne le multi-task en Windows.
Le multi-task en Windows
Un ordinateur, ça ne sait faire qu'une seule chose à la fois !
Windows, comme tout bon SE actuel se sert d'une méthode particulière afin de simuler un multi-task. En effet, un processeur ne sait que faire une chose à la fois. La technique est bien simple, il s'agit de créer un système de jeton et de le passer à chaque processus pour un certain temps selon leur priorité.
En ce moment même, vous utilisez votre navigateur préféré pour visiter le site du Zéro, mais cela n'empêche pas votre ordinateur de vaquer à d'autres occupations. Par exemple, vous êtes peut-être en train de copier un fichier, ou même juste en train d'avoir 3 fenêtres ouvertes sur le Bureau en ce moment. Windows doit rafraîchir leur contenu à toutes les x millisecondes afin de créer un sentiment de fluidité chez l'utilisateur.
Donc, suivant cet exemple, Windows aura un jeton à accorder à votre navigateur web pour tant de temps, puis suspendra ses calculs et opérations et donnera le jeton à un autre traitement. Lorsqu'il reviendra au navigateur, celui-ci sera autorisé à continuer ses opérations. Comme ça, tout le monde est content, mais surtout l'utilisateur qui désire ouvrir Google Chrome en même temps que MSN et Word, ainsi que Visual Studio 2010. Ne riez pas, c'est pas mal le scénario actuel de mon PC en ce moment... Tout ça pour dire qu'on commence la section sur les threads pour de vrai !
Les threads, enfin !
Les threads sont des exécutions que l'on sépare de l'exécution principale pour les raisons suivantes :
- Tâche exigeante (gros calculs, gros traitements, etc)...
- Tâche détachée (impression, recherche, etc)...
- Tâche bloquante (ça sent le réseau ici !)...
Créer un thread dans ces cas est utile afin de créer un certain parallélisme dans les exécutions.
Les threads rendent un code beaucoup plus complexe, non seulement à l'écriture, mais aussi au débogage ! Oh là là, que de frustration passées à programmer avec les threads. Cela dit, ils apportent énormément de puissance à une application ! Cette complexité vient du fait que le système de jeton est imprévisible ! Par exemple, il pourrait passer 3 secondes sur un thread A, mais 2 secondes sur un thread B. Bien-sûr, on ne parle pas de secondes ici, mais bien de nano-secondes. Un autre point frustrant sera que lors du déboggage, vous pourriez vous retrouver dans une méthode complètement différente de celle qui vous intéresse en un clin d'oeil, justement à cause que le système de jeton a changé de thread et vous a projeté dans la méthode qu'il exécute au moment même.
Le cas des tâches bloquantes...
Une application Windows Forms est amenée à se rafraîchir assez fréquemment. Lors de grosses opérations ou d'opérations synchrones qui bloquent, tout le temps de calcul est alloué à ces tâches et non plus au rafraîchissement. Après un certain temps, Windows déclare l'application comme "Ne répondant plus". Si vous planifiez de distribuer votre application, ce comportement est inacceptable, vous en conviendrez. C'est dans ce type de cas qu'on utilisera les threads. Imaginez afficher une belle animation sur un
Splash Screen alors que les ressources sont en chargement en arrière plan.
Les différents types de thread
Il est possible de créer deux types de threads, bien que cela revienne au même. Lors de l'instanciation de la classe
Thread, il est possible de garder la référence de l'objet, ou de la laisse flotter. Si on ne la récupère pas, on appellera ce thread "indépendant". Il sera créé, puis lancé immédiatement. Comme on ne gardera pas la référence, on ne pourra pas contrôler ce thread du tout. Il fera ce qu'il a à faire, sans que l'on puisse intervenir (sauf en utilisant quelques primitives de synchronisation que nous verrons plus tard).
Voici comment déclarer un thread indépendant :
Code : C# | new Thread(fonction).Start();
|
Puisqu'il n'y a pas d'opérateur d'affectation ( = ), on laisse partir la référence. Lorsque le thread se terminera, le Garbage Collector passera en arrière et se débarrassera de l'objet pour nous.
Le type dépendant est beaucoup plus fréquent. Il s'agit de garder la référence sur l'objet afin de pouvoir l'analyser, le tester, l'influencer. Vous connaissez déjà tous comment le créer, mais pour la forme, voici un exemple :
Code : C# | Thread nomThread = new Thread(fonction);
|
Cela peut porter à confusion, mais tous les threads d'une même application sont appelés Processus dans l'environnement Windows.Ceci provient du fait que Windows désire grouper tous les threads appartenant à une application au cas où ça tournerait mal. Cependant, le système de jeton mentionné ci-haut fonctionne au niveau des threads lui-même, et pas des processus. Cela signifie également que de mettre fin au thread principal met en effet fin aux threads enfants, indépendants ou dépendants, car on détruit carrément le processus et les threads qui le composent.
Comment lancer le thread ?
Tout d'abord, assurez-vous d'utilisez l'espace de nom (vous savez, les
using tout en haut de votre fichier .cs)
using System.Threading;.
Déclarer un nouveau thread va comme suit :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace TestThread
{
class Program
{
static void Main(string[] args)
{
//On initialise l'object Thread en lui passant la méthode
//à exécuter dans le nouveau thread. Ça vous rappelle pas
//certains delegates ça ?
Thread th = new Thread(Afficher);
//Un thread, ça ne part pas tout seul. Il faut lui indiquer de
//commencer l'exécution.
th.Start();
Console.ReadKey();
}
static void Afficher()
{
//Code tout bête qui affiche la lettre A 1000 fois.
for (int i = 0; i < 1000; i++)
{
Console.Write("A");
}
}
}
}
|
Ce code fonctionne bien dans le cas où on n'a aucun paramètre à passer. Il est un peu plus compliqué d'en passer, mais on s'en sort, vous verrez.
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace TestThread
{
class Program
{
static void Main(string[] args)
{
//Il faut créer un objet ParameterizedThreadStart dans le constructeur
//du thread afin de passer un paramètre.
Thread th = new Thread(new ParameterizedThreadStart(Afficher));
Thread th2 = new Thread(new ParameterizedThreadStart(Afficher));
//Lorsqu'on exécute le thread, on lui donne son paramètre de type Object.
th.Start("A");
th2.Start("B");
Console.ReadKey();
}
//La méthode prend en paramètre un et un seul paramètre de type Object.
static void Afficher(object texte)
{
for (int i = 0; i < 10000; i++)
{
//On écrit le texte passer en paramètre. N'oubliez pas de le caster
//car il s'agit d'un type Object, pas String.
Console.Write((string)texte);
}
Console.WriteLine("<------------Thread {0} terminé----------->", (string)texte);
}
}
}
|
Attention, la plus fréquente source d'erreur lors de l'utilisation de ce genre de thread est le paramètre. En effet, ce type de delegate a sa propre définition et il requiert un seul et unique paramètre qui sera de type object. Faîtes bien attention à transtyper (cast) vos variables correctement !
Cet exemple est parfait pour vous montrer comment les threads sont imprévisibles, et c'est ce qui les rend compliqués ! Je vous montre le résultat chez moi.
C'est pas bien beau tout ça. Je suis sûr que chez vous, c'est tout-à-fait différent. Même si je ne fais que le redémarrer, ce sera différent ! La leçon à retenir ici est que les threads sont imprévisibles, comme je l'ai expliqué plus haut. Pour preuve, j'ai relancé le même processus un peu plus tard, et voici le résultat :
On voit très bien que dans ce cas-ci, le Thread B a terminé en premier, ce qui prouve que le même code peut générer des résultats différents d'une exécution à l'autre, s'il est codé avec des threads !
Cas particuliers
Même si un thread s'exécute en deçà de votre programme principal, il reste que la méthode qu'il exécute fait partie de la classe à laquelle la méthode appartient. Cela signifie que l'accès aux variables globales et membres de votre classe lui seront accessibles sans problème.
Là où le problème se pose, c'est lorsque plusieurs threads devront accéder à la même variable, y faire des changements et des tests. Imaginez que votre thread A accède aux variables
nominateuret
dénominateur qui sont globales (à proscrire, mais bon). Le thread A a le temps de faire quelques tests, à savoir vérifier si le dénominateur n'est pas égal à zéro avant de procéder à une division. Tous les tests passent, mais juste au moment où le thread arrive pour effectuer l'opération, le thread B s'empare du jeton. Le thread B est chargé de réinitialiser le dénominateur à 0, et c'est ce qu'il fait. À ce moment là, le jeton revient au thread A qui tente d'effectuer la division. Oops, ça plante... C'est ce qu'on appelle un problème de synchronisation. Je ne vais pas vous mentir, ces problèmes sont rares. Il faut vraiment que vous soyez malchanceux. Il reste cependant important de bien synchroniser ses threads, surtout si l'on aspire à commercialiser le produit. Ainsi, plusieurs structures de synchronisation existent, et nous allons en survoler quelques unes.
Les mécanismes de synchronisation
Les variables de contrôle
Il peut sembler que les variables de contrôle soient un concept très poussé, mais pas du tout ! Il s'agit bêtement d'une variable globale que seul le thread principal modifiera et que les threads enfants contrôleront. Ce concept est particulièrement efficace dans le cas où le thread effectue une boucle infinie. Encore un fois, ça sent la programmation réseau ici. Je vous illustre le concept à l'aide d'un bête exemple.
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace VarControle
{
class Program
{
//Quelques variables à portée globale.
private static bool _quitter = false;
private static int _identificateur = 0;
static void Main(string[] args)
{
Console.Title = "Variables de contrôle";
//On crée un tableau de threads.
Thread[] threads = new Thread[5];
//On itère à travers le tableau afin de créer et lancer les threads.
for(int i = 0; i< threads.Length; i++)
{
//Création et lancement des threads.
threads[i] = new Thread(OperThread);
threads[i].Start();
//On laisse passer 500ms entre les création de thread.
Thread.Sleep(500);
}
//On demande à ce que tous les threads quittent.
_quitter = true;
Console.ReadKey();
}
static void OperThread()
{
//On donne au thread un identificateur unique.
int id = ++_identificateur;
Console.WriteLine("Début du thread {0}", id);
while (!_quitter)
{
//On fait des choses ici tant qu'on ne désire pas quitter...
Console.WriteLine("Thread {0} a le contrôle", id);
//On met le thread en état de sommeil pour 1000ms / 1s.
Thread.Sleep(1000);
}
Console.WriteLine("Thread {0} terminé", id);
}
}
}
|
Voici le résultat :
Ce qu'il faut comprendre de cet exemple, c'est que les variables de contrôle sont une bonne méthode afin d'influencer le comportement d'un thread, mais généralement seulement lorsque celui-ci est en boucle. Aussi, il est TRÈS important de retenir que seul le thread principal doit modifier la valeur de cette variable. Sinon, on pourrait retrouver des threads à toutes les sauces. Imaginez qu'à chaque itération, la boucle change la valeur de la variable de contrôle. On ne sait pas quand ou comment cela se produira et les problèmes feraient probablement vite leur apparition. Somme toute, il s'agit d'une bonne méthode à ne pas utiliser à outrance.
Avez-vous remarqué un bout de code qui ne vous semblait pas
thread-safe ? Si oui, vous comprendrez certainement l'utilité du prochain mécanisme de synchronisation.
Secret (cliquez pour afficher)
Code : C# | //On donne au thread un identificateur unique.
int id = ++_identificateur;
|
Ce bout de code n'est pas thread-safe, car on ne sait pas si un autre processus pourrait prendre le contrôle au mauvais moment. Si l'ordre de lancement est très important, cette ligne pourrait ne pas s'exécuter à temps.
Le lock
L'instruction
lock permet de verrouiller efficacement une ressource tant et aussi longtemps qu'un bloc d'instruction est en cours. Cela signifie que si d'autres threads tentent d'accéder à la même ressource en même temps, ils ne pourront pas. Cela ne signifie pas qu'ils planteront et se termineront, mais plutôt qu'ils passeront le jeton à un autre thread et attendront patiemment leur tour afin d'accéder à cette ressource.
Voici un bel exemple :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace Lock
{
class Program
{
//Variable témoin du lock.
private static Object _lock = new Object();
//Sert à initialiser des valeurs pseudos-aléatoires.
private static Random _rand = new Random((int)DateTime.Now.Ticks);
//Variable de contrôle.
private static bool _quitter = false;
//Variables globales étant affectées par les threads.
private static int _nominateur;
private static int _denominateur;
static void Main(string[] args)
{
Console.Title = "Démonstration des lock";
//On crée les threads.
Thread init = new Thread(Initialiser);
init.Start();
Thread reinit = new Thread(Reinitialiser);
reinit.Start();
Thread div = new Thread(Diviser);
div.Start();
//On les laisse travailler pendant 3 seconde.
Thread.Sleep(3000);
//Puis on leur demande de quitter.
_quitter = true;
Console.ReadKey();
}
private static void Initialiser()
{
//Boucle infinie contrôlée.
while (!_quitter)
{
//On verouille l'accès aux variables tant que l'on a pas terminé.
lock (_lock)
{
//Initialisation des valeurs.
_nominateur = _rand.Next(20);
_denominateur = _rand.Next(2, 30);
}
//On recommence dans 250ms.
Thread.Sleep(250);
}
}
private static void Reinitialiser()
{
//Boucle infinie contrôlée.
while (!_quitter)
{
//On verouille l'accès aux variables tant que l'on a pas terminé.
lock (_lock)
{
//Réinitialisation des valeurs.
_nominateur = 0;
_denominateur = 0;
}
//On recommence dans 300ms.
Thread.Sleep(300);
}
}
private static void Diviser()
{
//Boucle infinie contrôlée.
while (!_quitter)
{
//On verouille pendant les opérations.
lock (_lock)
{
//Erreur si le dénominateur est nul.
if (_denominateur == 0)
Console.WriteLine("Division par 0");
else
{
Console.WriteLine("{0} / {1} = {2}", _nominateur, _denominateur, _nominateur / (double)_denominateur);
}
}
//On recommence dans 275ms.
Thread.Sleep(275);
}
}
}
}
|
Résultat :
C'est bien comique parce que lorsque je vous ai préparé cet exemple, l'erreur dont je vous ai mentionné plus tôt s'est produite. Je n'avais alors pas englobé mon test du dénominateur dans l'instruction lock, ce qui a permis au thread en charge de réinitialiser les valeurs d'embarquer. Cela a produit une erreur de type NaN (Non-Numérique). Malheureusement, sur le coup, je n'ai pas fait de capture d'écran, et j'ai donc essayé de reproduire l'erreur, mais sans succès. C'est donc vous dire qu'avec les threads, il faut tout prévoir dès le départ, car l'apparition d'une erreur peut être un heureux (ou malheureux) hasard !
Donc, dans cet exemple, on voit que tout est bien protégé. Aucun thread ne peut venir interférer avec les autres. Remarquez la création d'une instance d'un objet de type
Object à la ligne 12. Cela est notre témoin de verrouillage. En réalité, n'importe quel objet qui se passe en référence peut servir de témoin de verrouillage. Comme nous avons travaillé avec des
int dans cet exemple et que ce type est passé par valeur, nous avons eu à créer cette variable.
Voici un exemple où une variable témoin est inutile :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace lockEx
{
class Program
{
static List<string> liste = new List<string>();
static void Main(string[] args)
{
for (int i = 0; i < 6; i++)
new Thread(Ajouter).Start();
}
static void Ajouter()
{
lock(liste)
liste.Add("abc");
}
}
}
|
Ici, on utilisera donc l'objet
liste qui se passe par référence, et qui est donc acceptable.
Il est possible de créer un lock en lui spécifiant un nom de type string. Cependant, Microsoft ne le recommande pas, car cette notation pourrait amener de la confusion. N'oubliez pas non plus qu'il est possible de faire de multiples lock qui ne protègent pas les mêmes ressources (indépendants les uns des autres). Il suffit de changer la variable témoin pour accommoder la situation.
Les Mutex
Les Mutex sont excessivement similaires aux
lock. Cependant, si vous désirez créer de nombreuses sections critiques indépendantes, les Mutex ont l'avantage d'être sous forme d'objets plutôt que d'instructions. Un petit exemple vous éclaira sur l'utilisation des Mutex.
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace MutexEx
{
class Program
{
private const int TAILLE_TABLEAU = 2;
//On crée les Mutex.
private static Mutex _muxMultiplier = new Mutex();
private static Mutex _muxDiviser = new Mutex();
//On crée les tableaux de valeurs.
private static int[] _valDiv = new int[TAILLE_TABLEAU];
private static int[] _valMul = new int[TAILLE_TABLEAU];
//Objet Random et variable de contrôle.
private static Random _rand = new Random((int)DateTime.Now.Ticks);
private static bool _quitter = false;
static void Main(string[] args)
{
Console.Title = "Exemple de Mutex";
//On crée et on démarre les threads.
Thread init = new Thread(Initialiser);
init.Start();
Thread mul = new Thread(Multiplier);
mul.Start();
Thread div = new Thread(Diviser);
div.Start();
//On laisse les threads fonctionner un peu...
Thread.Sleep(3000);
//On demande à ce que les opérations se terminent.
_quitter = true;
Console.ReadKey();
}
private static void Initialiser()
{
while (!_quitter)
{
//On demande au thread d'attendre jusqu'à ce qu'il ait le contrôle sur les Mutex.
_muxMultiplier.WaitOne();
_muxDiviser.WaitOne();
for (int i = 0; i < TAILLE_TABLEAU; i++)
{
//On assigne au tableau de nouvelles valeurs.
_valMul[i] = _rand.Next(2, 20);
_valDiv[i] = _rand.Next(2, 20);
}
Console.WriteLine("Nouvelles valeurs !");
//On relâche les Mutex
_muxDiviser.ReleaseMutex();
_muxMultiplier.ReleaseMutex();
//On tombe endormi pour 100ms.
Thread.Sleep(100);
}
}
private static void Multiplier()
{
while (!_quitter)
{
//On demande le Mutex de multiplication.
_muxMultiplier.WaitOne();
//On multiplie.
Console.WriteLine("{0} x {1} = {2}", _valMul[0], _valMul[1], _valMul[0] * _valMul[1]);
//On relâche le Mutex.
_muxMultiplier.ReleaseMutex();
//On tombe endormi pour 200ms.
Thread.Sleep(200);
}
}
private static void Diviser()
{
while (!_quitter)
{
//On demande le Mutex de division.
_muxDiviser.WaitOne();
//On divise.
Console.WriteLine("{0} / {1} = {2}", _valDiv[0], _valDiv[1], _valDiv[0] * _valDiv[1]);
//On relâche le Mutex de Division.
_muxDiviser.ReleaseMutex();
//On tombe endormi pour 200ms.
Thread.Sleep(200);
}
}
}
}
|

Si vous avez fait un peu de programmation en Win32 (langage C), vous pouvez voir la lignée directe des Mutex du .NET et des
CRITICAL_SECTION du Win32. Sinon, vous voyez que les Mutex ont la même fonction que l'instruction
lock en un peu plus verbeux. Je tiens cependant à vous avertir que de ne pas relâcher un Mutex peut faire planter votre application, donc faîtes attention à cela.
SemaphoreSlim
Le SemaphoreSlim sert à contrôler l'accès d'une ressource limitée. Jusqu'à maintenant, les mécanismes de synchronisation dont nous avons parlé ont surtout servi à limiter une ressource à un accès mutuellement exclusif entre des threads concurrents. Quant est-il si l'on veut partager une ressource, mais à travers plusieurs threads simultanément ? Cependant, on aimerait garder un nombre maximal d'accès concurrent à la ressource. Les sémaphores existent pour cette raison. En C# .NET, il existe deux types de sémaphores. Le classique
Semaphore et le
SemaphoreSlim. La différence provient de la complexité de l'objet et des mécanismes internes. Le Semaphore utilise un wrapper autour de l'objet Semaphore du Win32 et rend donc disponible ses fonctionnalités en .NET. Le SemaphoreSlim, lui, est plutôt utilisé lors de courtes durées d'attente et utilise les mécanismes propres au
CLR.
Je ne montrerai que le SemaphoreSlim, les deux se ressemblant beaucoup. Cependant, le SemaphoreSlim reste le plus facile et le plus léger à implémenter. Pour plus d'information sur la différence, veuillez lire cet article sur
MSDN. Peu importe la version qui est choisi, vous pouvez voir les Sémaphores comme un "doorman" dans une boîte de nuit. La place à l'intérieur est limitée et le doorman devra contrôler l'accès à la ressource.
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace SemaphoreSlimEx
{
class Program
{
//Déclaration du SemaphoreSlim qui prendra en paramètre le nombre de places disponibles.
static SemaphoreSlim doorman = new SemaphoreSlim(3);
static void Main(string[] args)
{
Console.Title = "Exemple de SemaphoreSlim";
//Création des threads.
for (int i = 0; i < 10; i++)
new Thread(Entrer).Start(i);
Console.ReadKey();
}
static void Entrer(object n)
{
Console.WriteLine("La personne #{0} veut entrer", n);
//Le doorman attendra qu'il y ait de la place.
doorman.Wait();
Console.WriteLine("#{0} vient d'entrer dans le bar", n);
Thread.Sleep((int)n * 1000);
Console.WriteLine("#{0} a quitté le building !", n);
//Le doorman peut maintenant faire entrer quelqu'un d'autre.
doorman.Release();
}
}
}
|
Le Join()
C'est le dernier mécanisme de synchronisation dont je parlerai. Il s'agit très simplement d'attendre la fin d'un autre thread afin de continuer le thread dans lequel le
Join() est défini. Cela en fait une méthode bloquante qui pourrait vous causer des problèmes en Windows Forms.
Petit exemple :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace TestThread
{
class Program
{
static void Main(string[] args)
{
Thread th = new Thread(new ParameterizedThreadStart(Afficher));
Thread th2 = new Thread(new ParameterizedThreadStart(Afficher));
th.Start("A");
//On attend la fin du thread A avant de commencer le thread B.
th.Join();
th2.Start("B");
Console.ReadKey();
}
static void Afficher(object texte)
{
for (int i = 0; i < 10000; i++)
{
Console.Write((string) texte);
}
}
}
}
|
Le Abort()
Bon, après avoir vu comment bien synchroniser ses threads, voyons ce que vous ne devez
PAS faire !!!
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadStop
{
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(Test);
thread.Start();
Thread.Sleep(100);
//On tue le processus. À NE PAS FAIRE !
thread.Abort();
Console.ReadKey();
}
public static void Test()
{
for(int i = 0; i < 10000; i++)
Console.WriteLine(i);
}
}
}
|
Aux premiers abords, cela semble assez facile à faire, et semble sans grandes conséquences. En fait, vous avez probablement raison dans cet exemple. Cependant, ne prenez pas l'habitude de faire terminer vos threads si abruptement, car il se pourrait que cela vous cause des erreurs éventuellement. En effet, vous ne fermez pas votre ordinateur en débranchant la prise du mur, n'est-ce pas ? (

N'est-ce pas ?). C'est le même principe ici. Si vous étiez en train de faire quelque chose de vraiment important, et que le thread principal choisirait ce moment pour arrêter le thread, l'application en entier pourrait devenir instable et planter. Puisqu'on ne veut pas cela, il vaut mieux utiliser le
Join() et une variable de contrôle, comme dans l'exemple suivant :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadStop
{
class Program
{
private static bool _continuer = true;
static void Main(string[] args)
{
Thread thread = new Thread(Test);
thread.Start();
Thread.Sleep(100);
//On demande au thread de s'arrêter au prochain passage d'un moment qui semble naturel.
_continuer = false;
//On attend que le thread se termine.
thread.Join();
Console.ReadKey();
}
public static void Test()
{
//On fait 10 000 itérations, tant et aussi longtemps que l'on peut continuer (variable de contrôle).
for(int i = 0; i < 10000 && _continuer; i++)
Console.WriteLine(i);
}
}
}
|
Et voilà, on se sent toujours mieux quand on fait quelque chose de bien, non ? Comme ça, si le thread a besoin de temps pour bien terminer ses opérations (appeler quelques
Dispose(), ou fermer des connexions TCP), il le pourra. Utilisez donc les
Join() et pas les
Abort(). Les
Abort(), c'est mal

...
Lorsque vous fermez l'application, Windows tend à simplement appeler Abort() sur les threads en fonction. Ce comportement est hérité du Win32 dont le .NET recouvre à l'aide de wrappers (il s'agit d'une longue histoire, croyez-moi). C'est aussi pourquoi on évitera autant que possible l'usage de threads indépendants, car ils sont moins contrôlables. Prévoyez donc attendre les threads actifs lorsque vous quitter votre application, car comme nous l'avons dit, les Abort(), c'est mal. Quoique avec le Garbage Collector de .NET.... NON, NON, C'EST MAL !
Nous sommes maintenant prêts à aborder le sujet du multi-tâche en Windows Forms ! Je vous montrerai comment éviter que cela ne vire en catastrophe, ne craignez rien.