Ce document a pour but de présenter les threads avec .NET. Il parle des traitements
multi-threadés ainsi que de l'interaction entre un thread et l'interface utilisateurs.
Le multithreading est utilisé un peu partout. Vous voulez faire un application réseau et
vous avez besoin de traiter les données de façon parrallèle, ou bien vous voulez
parrallé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.
2. 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 :
Utilisation de l'espace de nom System.Threading
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.
Exemple
using System;
using System.Threading;
namespace ThreadTest
{
class Class1
{
int cmpt;
[STAThread]
staticvoid Main(string[] args)
{
Class1 c = new Class1();
c.LaunchTest();
}
publicvoid 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();
}
publicvoid NextStep(){
while(true) {
cmpt++;
// Suspend le thread de comptage pendant 1s
Thread.Sleep(500);
}
}
}
}
Si vous lancer 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 :
Exemple
using System;
using System.Threading;
using System.Reflection;
namespace ThreadTest {
class Class1 {
int cmpt;
[STAThread]
staticvoid Main(string[] args) {
Console.Out.WriteLine();
Class1 c = new Class1();
c.LaunchTest();
}
publicvoid 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();
}
publicvoid 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 de d'arrêter le thread.
3. 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écisemment 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é.
Synchronisation
using System;
using System.Threading;
namespace ThreadTest {
class Class1 {
int cmpt;
bool ok1;
bool ok2;
delegatevoid CmptIncEventHandler();
privateevent CmptIncEventHandler Thread1Event;
privateevent CmptIncEventHandler Thread2Event;
Thread t1;
Thread t2;
public Class1() {
// Evènement permettant la synchronisation inter threadsthis.Thread1Event += new CmptIncEventHandler(Th1OK);
this.Thread2Event += new CmptIncEventHandler(Th2OK);
}
[STAThread]
staticvoid Main(string[] args) {
Console.Out.WriteLine();
Class1 c = new Class1();
c.LaunchTest();
}
publicvoid 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 thread1while(cmpt<10){
Thread.Sleep(0);
}
t1.Abort();
t2.Abort();
Console.In.ReadLine();
}
publicvoid Th1OK() {
ok2=true;
}
publicvoid Th2OK() {
ok1=true;
}
publicvoid Th1(){
while(true) {
if(ok1) {
ok1=false;
cmpt++;
Console.Out.WriteLine("[1]cmpt=" + cmpt.ToString());
Thread1Event();
}
Thread.Sleep(10);
}
}
publicvoid Th2(){
while(true) {
if(ok2) {
ok2=false;
cmpt++;
Console.Out.WriteLine("[2]cmpt=" + cmpt.ToString());
Thread2Event();
}
Thread.Sleep(10);
}
}
}
}
Vous contastez 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".
4. 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.
Utilisation du lock
using System;
using System.Threading;
namespace ThreadTest {
class MaRessource {
int m_mavar;
public MaRessource(){
m_mavar = 0;
}
publicint MaVar {
get{return m_mavar;}
set{m_mavar = value;}
}
}
class Class1 {
MaRessource res;
public Class1() {
res = new MaRessource();
}
[STAThread]
staticvoid Main(string[] args) {
Console.Out.WriteLine();
Class1 c = new Class1();
c.LaunchTest();
}
publicvoid LaunchTest(){
Thread t1 = new Thread(new ThreadStart(Th1));
t1.Start(); // Lance le thread1lock(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();
}
publicvoid 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 :
Résultat
[1]07:31:58 > 0
[2]07:32:00 > 50
[3]07:32:00 > 50
Comment .NET a réagit ? 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'aperoit 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 deuxè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.
5. 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.
5.1. 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é
using System;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
using System.Threading;
namespace ThreadTest
{
publicclass Form1 : System.Windows.Forms.Form
{
// Déclaration du délégué nécessaire à la mise à jourprivatedelegatevoid 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);
}
protectedoverridevoid Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
privatevoid 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);
}
privatevoid 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();
}
privatevoid ThreadProcess() {
for(int i = 0 ; i < 11 ; i++)
{
// Déclenche la mise à jour de l'interfacethis.Invoke(this.ProgressBarDelegate,newobject[] {i});
Thread.Sleep(1000);
}
}
privatevoid UpdateProgressBar(int step)
{
// Mise à jour de la barre de progression
progressBar1.Value = step;
}
}
}
5.2. 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é.
privatedelegatevoid InvokeMethod(int step);
Ensuite vous pouvez utiliser les deux méthodes BeginInvoke et EndInvoke.
IAsyncResult res = progressBar1.BeginInvoke(new InvokeMethod(UpdateProgressBar),newobject[] {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
{
publicclass Form2 : System.Windows.Forms.Form
{
privatedelegatevoid InvokeMethod(int step);
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Button button1;
private System.ComponentModel.Container components = null;
public Form2()
{
InitializeComponent();
}
protectedoverridevoid Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
privatevoid 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);
}
privatevoid button1_Click(object sender, System.EventArgs e) {
this.progressBar1.Minimum=0;
this.progressBar1.Maximum=10;
Thread t = new Thread(new ThreadStart(ThreadProcess));
t.Start();
}
privatevoid ThreadProcess() {
IAsyncResult res;
for(int i = 0 ; i < 11 ; i++) {
res = progressBar1.BeginInvoke(new InvokeMethod(UpdateProgressBar),newobject[] {i});
progressBar1.EndInvoke(res);
Thread.Sleep(1000);
}
}
privatevoid UpdateProgressBar(int step) {
this.progressBar1.Value = step;
}
}
}
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.
6.1. La classe Monitor
La classe Monitor est très simple d'utilisation. Jusqu'à présent
on a vu comment faire pour parralléliser les traitements,
maintenant nous allons forcer un traitement à être synchrone.
A quoi ça peut 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 appelera 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.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. A 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).
6.2. 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 lesexclusions peuvent se faire
inter-applications.
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é 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();
Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.