I. Introduction▲
Le multithreading est utilisé un peu partout. Vous voulez faire une application réseau et vous avez besoin de traiter les données de façon parallèle, ou bien vous voulez paralléliser des traitements de calcul, mettre à jour une interface graphique pendant qu'un traitement se déroule… Lorsque vous faites du multithreading, des « problèmes » apparaissent comme le partage de ressources ou l'interbloquage. Vous trouverez dans ce document comment manipuler les threads et comment gérer les problèmes liés au multithreading.
II. Création d'un thread▲
Afin de pouvoir travailler avec les threads il faut utiliser l'espace de nom « System.Threading ». Dans toutes vos classes utilisant les threads vous devez mettre :
using
System.
Threading;
Pour un premier exemple nous allons créer un thread qui rajoute 1 à une variable. Nous y verrons comment « endormir » un thread pour une durée déterminée, comment mettre un thread en pause, comment le relancer et comment l'interrompre.
using
System;
using
System.
Threading;
namespace
ThreadTest
{
class
Class1
{
int
cmpt;
[STAThread]
static
void
Main
(
string
[]
args)
{
Class1 c =
new
Class1
(
);
c.
LaunchTest
(
);
}
public
void
LaunchTest
(
){
Thread t =
new
Thread
(
new
ThreadStart
(
NextStep));
t.
Start
(
);
for
(
cmpt=
0
;
cmpt<
10
;
) {
Console.
Out.
WriteLine
(
cmpt.
ToString
(
));
// Suspend le thread d'incrémentation
t.
Suspend
(
);
// Suspend le thread principal pendant 1/2 seconde
Thread.
Sleep
(
500
);
// Relance le thread de comptage
t.
Resume
(
);
}
// Arrête le thread de comptage
t.
Abort
(
);
Console.
In.
ReadLine
(
);
}
public
void
NextStep
(
){
while
(
true
) {
cmpt++;
// Suspend le thread de comptage pendant 1s
Thread.
Sleep
(
500
);
}
}
}
}
Si vous lancez l'application plusieurs fois vous verrez que l'affichage varie. Donc aucune synchronisation entre les threads.
La méthode statique Thread.Sleep permet de bloquer le thread dans lequel le code s'exécute pendant une durée donnée en paramètre (durée en millisecondes). Lorsque vous faites un Threads.Sleep(500) dans le for cela n'arrête pas la boucle de la méthode NextStep. Vous vous en rendez plus compte avec ce code :
using
System;
using
System.
Threading;
using
System.
Reflection;
namespace
ThreadTest {
class
Class1 {
int
cmpt;
[STAThread]
static
void
Main
(
string
[]
args) {
Console.
Out.
WriteLine
(
);
Class1 c =
new
Class1
(
);
c.
LaunchTest
(
);
}
public
void
LaunchTest
(
){
Thread t =
new
Thread
(
new
ThreadStart
(
NextStep));
cmpt=
0
;
t.
Start
(
);
for
(
int
i=
0
;
i<
10
;
i++
) {
Console.
Out.
WriteLine
(
cmpt.
ToString
(
));
// Suspend le thread d'incrémentation
t.
Suspend
(
);
// Suspend le thread principal pendant 1/2 seconde
Thread.
Sleep
(
500
);
// Relance le thread de comptage
t.
Resume
(
);
}
// Arrête le thread de comptage
t.
Abort
(
);
Console.
In.
ReadLine
(
);
}
public
void
NextStep
(
){
while
(
true
) {
cmpt++;
}
}
}
}
La méthode Suspend() permet de suspendre le thread.
La méthode Resume() permet de relancer le thread.
La méthode Abrort() permet d'arrêter le thread.
III. Synchronisation de deux threads▲
Dans la partie précédente, les threads ne sont pas du tout synchronisés. Le but de cette partie est de montrer une façon de synchroniser deux threads. Pour cela il faut utiliser les délégués ou plus précisément les évènements.
Afin de bien comprendre, nous reprendrons l'exemple du chapitre 2 et le résultat ne sera affiché que lorsque le compteur sera incrémenté.
using
System;
using
System.
Threading;
namespace
ThreadTest {
class
Class1 {
int
cmpt;
bool
ok1;
bool
ok2;
delegate
void
CmptIncEventHandler
(
);
private
event
CmptIncEventHandler Thread1Event;
private
event
CmptIncEventHandler Thread2Event;
Thread t1;
Thread t2;
public
Class1
(
) {
// Évènement permettant la synchronisation inter threads
this
.
Thread1Event +=
new
CmptIncEventHandler
(
Th1OK);
this
.
Thread2Event +=
new
CmptIncEventHandler
(
Th2OK);
}
[STAThread]
static
void
Main
(
string
[]
args) {
Console.
Out.
WriteLine
(
);
Class1 c =
new
Class1
(
);
c.
LaunchTest
(
);
}
public
void
LaunchTest
(
){
t1 =
new
Thread
(
new
ThreadStart
(
Th1));
t2 =
new
Thread
(
new
ThreadStart
(
Th2));
cmpt=
0
;
t1.
Start
(
);
// Lance le thread1
t2.
Start
(
);
// Lance le thread2
ok1=
true
;
// Commence par le thread1
while
(
cmpt<
10
){
Thread.
Sleep
(
0
);
}
t1.
Abort
(
);
t2.
Abort
(
);
Console.
In.
ReadLine
(
);
}
public
void
Th1OK
(
) {
ok2=
true
;
}
public
void
Th2OK
(
) {
ok1=
true
;
}
public
void
Th1
(
){
while
(
true
) {
if
(
ok1) {
ok1=
false
;
cmpt++;
Console.
Out.
WriteLine
(
"[1]cmpt="
+
cmpt.
ToString
(
));
Thread1Event
(
);
}
Thread.
Sleep
(
10
);
}
}
public
void
Th2
(
){
while
(
true
) {
if
(
ok2) {
ok2=
false
;
cmpt++;
Console.
Out.
WriteLine
(
"[2]cmpt="
+
cmpt.
ToString
(
));
Thread2Event
(
);
}
Thread.
Sleep
(
10
);
}
}
}
}
Vous obtiendrez un résultat du style :
[1]
cmpt=
1
[2]
cmpt=
2
[1]
cmpt=
3
[2]
cmpt=
4
[1]
cmpt=
5
[2]
cmpt=
6
[1]
cmpt=
7
[2]
cmpt=
8
[1]
cmpt=
9
[2]
cmpt=
10
Vous constatez que les threads traitent la même donnée l'un après l'autre.
Le fait de travailler sur la même donnée ou même ressource peut poser problème. Dans l'exemple précédent, le problème ne se pose pas, car les deux threads sont synchronisés et donc ne peuvent pas accéder à la même ressource en même temps. Qu'en est-il de deux processus non synchronisés ? Nous verrons cela dans le chapitre suivant « Partage de ressources ».
IV. Utilisation partagée des ressources▲
Cette partie montre le cas de partage de ressources. Comment éviter que deux threads altèrent la ressource en même temps ? Ceci est faisable par le mot clé lock qui permet de bloquer une ressource. Son utilisation est relativement simple, vous mettez le code utilisant la ressource dans lebloc du lock.
using
System;
using
System.
Threading;
namespace
ThreadTest {
class
MaRessource {
int
m_mavar;
public
MaRessource
(
){
m_mavar =
0
;
}
public
int
MaVar {
get
{
return
m_mavar;}
set
{
m_mavar =
value
;}
}
}
class
Class1 {
MaRessource res;
public
Class1
(
) {
res =
new
MaRessource
(
);
}
[STAThread]
static
void
Main
(
string
[]
args) {
Console.
Out.
WriteLine
(
);
Class1 c =
new
Class1
(
);
c.
LaunchTest
(
);
}
public
void
LaunchTest
(
){
Thread t1 =
new
Thread
(
new
ThreadStart
(
Th1));
t1.
Start
(
);
// Lance le thread1
lock
(
res){
Console.
Out.
WriteLine
(
DateTime.
Now.
ToString
(
"[1]hh:mm:ss > "
) +
res.
MaVar);
res.
MaVar =
10
;
Thread.
Sleep
(
2000
);
}
lock
(
res){
Console.
Out.
WriteLine
(
DateTime.
Now.
ToString
(
"[3]hh:mm:ss > "
) +
res.
MaVar);
}
Console.
In.
ReadLine
(
);
}
public
void
Th1
(
){
Thread.
Sleep
(
10
);
lock
(
res){
res.
MaVar=
50
;
Console.
Out.
WriteLine
(
DateTime.
Now.
ToString
(
"[2]hh:mm:ss > "
) +
res.
MaVar);
}
}
}
}
En résultat nous obtenons :
[1]
07
:31
:58
>
0
[2]
07
:32
:00
>
50
[3]
07
:32
:00
>
50
Comment .NET a-t-il réagi ? Le sleep dans la méthode Th1 permet de laisser le temps au thread principal de rentrer dans le lock. Lorsque le lock de la méthode Th1 est effectué, il s'aperçoit que la ressource res est déjà bloquée. Il met donc le lock sur la liste d'attente jusqu'à ce que le lock actuel ne soit plus. Même réaction lorsque le deuxième lock de LaunchTest est appelé, il est mis en attente jusqu'à ce que le lock de Th1 soit terminé. Ainsi une même ressource ne peut être utilisée en même temps dans différents threads.
V. Mettre à jour l'IHM pendant un traitement▲
Pour la mise à jour d'une interface graphique, il faut aussi passer par les délégués, mais cela ne suffit pas. Pour fonctionner correctement, vous devez utiliser soit la méthode Invoke soit BeginInvoke.
Pour exemple, un thread incrémentera une barre de progression.
V-A. Méthode Invoke▲
La méthode Invoke est « bloquée », c'est-à-dire rend la main qu'après le retour de la méthode appelée par le délégué. Attention cette ne peut être qu'à partir du moment que le contrôle appelant n'a pas été créé dans le thread dans lequel le Invoke est fait : ce cas peut se produire lors de la création dynamique de contrôles. Pour résoudre ce problème, voir le point suivant.
La première chose à faire est de déclarer le délégué :
private
delegate
void
ProgressBarDelegateHandler
(
int
step);
private
ProgressBarDelegateHandler ProgressBarDelegate;
Ensuite il faut une méthode de même prototype que le délégué :
private
void
UpdateProgressBar
(
int
step)
{
// Mise à jour de la barre de progression
progressBar1.
Value =
step;
}
Le lien entre le délégué et la méthode de mise à jour se fait de la façon suivante :
ProgressBarDelegate =
new
ProgressBarDelegateHandler
(
UpdateProgressBar);
Dans la méthode tournant dans le thread séparé, l'appel de la mise à jour se fait avec la méthode Invoke de la façon suivante :
this
.
Invoke
(
this
.
ProgressBarDelegate,
new
object
[]
{
i}
);
Voici le code complet :
using
System;
using
System.
Drawing;
using
System.
ComponentModel;
using
System.
Windows.
Forms;
using
System.
Threading;
namespace
ThreadTest
{
public
class
Form1 :
System.
Windows.
Forms.
Form
{
// Déclaration du délégué nécessaire à la mise à jour
private
delegate
void
ProgressBarDelegateHandler
(
int
step);
private
ProgressBarDelegateHandler ProgressBarDelegate;
private
System.
Windows.
Forms.
Button button1;
private
System.
Windows.
Forms.
ProgressBar progressBar1;
private
System.
ComponentModel.
Container components =
null
;
public
Form1
(
)
{
InitializeComponent
(
);
// Sur appel du délégué, la méthode UpdateProgressBar est donc appelé
ProgressBarDelegate =
new
ProgressBarDelegateHandler
(
UpdateProgressBar);
}
protected
override
void
Dispose
(
bool
disposing )
{
if
(
disposing )
{
if
(
components !=
null
)
{
components.
Dispose
(
);
}
}
base
.
Dispose
(
disposing );
}
private
void
InitializeComponent
(
)
{
this
.
button1 =
new
System.
Windows.
Forms.
Button
(
);
this
.
progressBar1 =
new
System.
Windows.
Forms.
ProgressBar
(
);
this
.
SuspendLayout
(
);
this
.
button1.
Location =
new
System.
Drawing.
Point
(
432
,
96
);
this
.
button1.
Name =
"button1"
;
this
.
button1.
Size =
new
System.
Drawing.
Size
(
96
,
40
);
this
.
button1.
TabIndex =
0
;
this
.
button1.
Text =
"GO"
;
this
.
button1.
Click +=
new
System.
EventHandler
(
this
.
button1_Click);
this
.
progressBar1.
Location =
new
System.
Drawing.
Point
(
16
,
104
);
this
.
progressBar1.
Name =
"progressBar1"
;
this
.
progressBar1.
Size =
new
System.
Drawing.
Size
(
344
,
24
);
this
.
progressBar1.
TabIndex =
1
;
this
.
AutoScaleBaseSize =
new
System.
Drawing.
Size
(
5
,
13
);
this
.
ClientSize =
new
System.
Drawing.
Size
(
568
,
301
);
this
.
Controls.
Add
(
this
.
progressBar1);
this
.
Controls.
Add
(
this
.
button1);
this
.
Name =
"Form1"
;
this
.
Text =
"Form1"
;
this
.
ResumeLayout
(
false
);
}
private
void
button1_Click
(
object
sender,
System.
EventArgs e) {
progressBar1.
Minimum =
0
;
progressBar1.
Maximum =
10
;
// Lancement du thread
Thread t =
new
Thread
(
new
ThreadStart
(
ThreadProcess));
t.
Start
(
);
}
private
void
ThreadProcess
(
) {
for
(
int
i =
0
;
i <
11
;
i++
)
{
// Déclenche la mise à jour de l'interface
this
.
Invoke
(
this
.
ProgressBarDelegate,
new
object
[]
{
i}
);
Thread.
Sleep
(
1000
);
}
}
private
void
UpdateProgressBar
(
int
step)
{
// Mise à jour de la barre de progression
progressBar1.
Value =
step;
}
}
}
V-B. Méthode BeginInvoke▲
Contrairement à la méthode Invoke, la méthode BeginInvoke n'est pas « bloquée ». Par contre une seconde méthode permet d'attendre la fin de l'exécution de la méthode lancée : EndInvoke. De plus BeginInvoke peut être appelée de n'importe quel thread.
Tout comme pour la méthode Invoke, il faut déclarer le délégué :
private
delegate
void
InvokeMethod
(
int
step);
Ensuite vous pouvez utiliser les deux méthodes BeginInvoke et EndInvoke :
IAsyncResult res =
progressBar1.
BeginInvoke
(
new
InvokeMethod
(
UpdateProgressBar),
new
object
[]
{
i}
);
progressBar1.
EndInvoke
(
res);
Afin que vous puissiez tester, voici le code complet :
using
System;
using
System.
Drawing;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Windows.
Forms;
using
System.
Threading;
namespace
ThreadTest
{
public
class
Form2 :
System.
Windows.
Forms.
Form
{
private
delegate
void
InvokeMethod
(
int
step);
private
System.
Windows.
Forms.
ProgressBar progressBar1;
private
System.
Windows.
Forms.
Button button1;
private
System.
ComponentModel.
Container components =
null
;
public
Form2
(
)
{
InitializeComponent
(
);
}
protected
override
void
Dispose
(
bool
disposing )
{
if
(
disposing )
{
if
(
components !=
null
)
{
components.
Dispose
(
);
}
}
base
.
Dispose
(
disposing );
}
private
void
InitializeComponent
(
)
{
this
.
progressBar1 =
new
System.
Windows.
Forms.
ProgressBar
(
);
this
.
button1 =
new
System.
Windows.
Forms.
Button
(
);
this
.
SuspendLayout
(
);
this
.
progressBar1.
Location =
new
System.
Drawing.
Point
(
8
,
16
);
this
.
progressBar1.
Name =
"progressBar1"
;
this
.
progressBar1.
Size =
new
System.
Drawing.
Size
(
344
,
24
);
this
.
progressBar1.
TabIndex =
3
;
this
.
button1.
Location =
new
System.
Drawing.
Point
(
424
,
8
);
this
.
button1.
Name =
"button1"
;
this
.
button1.
Size =
new
System.
Drawing.
Size
(
96
,
40
);
this
.
button1.
TabIndex =
2
;
this
.
button1.
Text =
"GO"
;
this
.
button1.
Click +=
new
System.
EventHandler
(
this
.
button1_Click);
this
.
AutoScaleBaseSize =
new
System.
Drawing.
Size
(
5
,
13
);
this
.
ClientSize =
new
System.
Drawing.
Size
(
544
,
285
);
this
.
Controls.
Add
(
this
.
progressBar1);
this
.
Controls.
Add
(
this
.
button1);
this
.
Name =
"Form2"
;
this
.
Text =
"Form2"
;
this
.
ResumeLayout
(
false
);
}
private
void
button1_Click
(
object
sender,
System.
EventArgs e) {
this
.
progressBar1.
Minimum=
0
;
this
.
progressBar1.
Maximum=
10
;
Thread t =
new
Thread
(
new
ThreadStart
(
ThreadProcess));
t.
Start
(
);
}
private
void
ThreadProcess
(
) {
IAsyncResult res;
for
(
int
i =
0
;
i <
11
;
i++
) {
res =
progressBar1.
BeginInvoke
(
new
InvokeMethod
(
UpdateProgressBar),
new
object
[]
{
i}
);
progressBar1.
EndInvoke
(
res);
Thread.
Sleep
(
1000
);
}
}
private
void
UpdateProgressBar
(
int
step) {
this
.
progressBar1.
Value =
step;
}
}
}
private
delegate
void
ProgressBarDelegateHandler
(
int
step);
VI. Les exclusions mutuelles▲
En complément du chapitre 4, il existe deux classes permettant de gérer le partage de ressources. Ces deux classes sont la classe Monitor et la classe Mutex.
VI-A. La classe Monitor▲
La classe Monitor est très simple d'utilisation. Jusqu'à présent on a vu comment faire pour paralléliser les traitements, maintenant nous allons forcer un traitement à être synchrone. À quoi ça peut-il servir ? Prenez le cas où votre application déclenche un traitement et vous devez attendre la fin de ce traitement pour pouvoir continuer. Mais voilà, le traitement que vous déclenchez se fait en arrière-plan et vous n'avez pas de contrôle dessus si ce n'est un évènement vous disant qu'il est fini.
Prenons pour exemple un composant que l'on appellera ComponentUsed qui possède une méthode Start() qui déclenche le traitement dans un thread séparé : le code continue après l'appel de Start même si le traitement n'est pas fini. Un évènement EventFinish vous indique que le traitement est terminé.
La classe Monitor ne s'instancie pas. Les méthodes disponibles sont Enter, TryEnter, Exit, Pulse, PulseAll, Wait. Les méthodes Enter/TryEnter et Exit vont ensemble ainsi que Pulse/PulseAll et Wait vont ensemble.
Monitor.Enter/Monitor.Exit représente la même chose que le mot clé lock. Cela permet de dire qu'une zone est critique et empêche deux threads d'entrer dans la zone en même temps.
Monitor.
Enter
(
this
);
try
{
// ...
}
catch
(
Exception){
/*empty catch*/
}
finally
{
Monitor.
Exit
(
this
);
}
// est identique à
lock
(
this
)
{
// ...
}
Monitor.Wait permet d'attendre un signal indiquant que le processus peut repartir. Pour reprendre notre exemple, vous effectuez un Monitor.Wait() juste après le ComponentUsed.Start(). L'exécution sera bloquée. Dans l'évènement EventFinish vous faites un Monitor.Pulse() ainsi vous débloquez le thread bloqué par Monitor.Wait().
Si vous faites plusieurs Monitor.Wait(), ces derniers sont empilés. À chaque appel de Monitor.Pulse() vous libérez le plus ancien. Si vous voulez les débloquer tous, vous pouvez faire un Monitor.PulseAll();
Dans le cas où vous ne voulez pas attendre indéfiniment un Monitor.Pulse() vous pouvez indiquer une durée en millisecondes d'attente maximale à la méthode Monitor.Wait(object obj, int millisecond).
VI-B. La classe Mutex▲
La classe Mutex est similaire aux méthodes Enter et Exit de la classe Monitor, mais, mais de façon bien plus large : grâce à la classe Mutex les exclusions peuvent se faire interapplications.
Pour cela vous devez utiliser les mutex nommés.
Mutex m =
new
Mutex
(
"MonAppli de test"
);
Afin de prendre possession du mutex vous devez utiliser la méthode WaitOne(). Cette méthode attend que le mutex soit disponible pour en prendre possession. Afin de ne pas être bloquée indéfiniment, elle peut prendre en paramètre une durée maximale d'attente. Si l'objet reçoit un signal avant le temps écoulé alors la méthode retourne true sinon false.
Pour libérer le mutex vous devez utiliser la méthode ReleaseMutex();
Mutex m =
new
Mutex
(
"MonAppli de test"
);
m.
ReleaseMutex
(
);