En.Net, les opérations cross-threads sont interdites. Cela signifie qu'un Thread ne peut modifier les propriétés d'objet qu'un autre Thread a créé.
Prenons par exemple, un Thread qui voudrait modifier la valeur d'un label d'une IHM .
using System; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace CrossThreadOp { public class MainForm : Form { private Thread t; private Label labelTest; public Main Form() { InitializeComponent(); t = new Thread(new ThreadStart(ThreadMethod)); t.Start(); } private void InitializeComponent() { this.labelTest = new System.Windows.Forms.Label(); this.labelTest.AutoSize = true ; this.labelTest.Location = new System.Drawing.Point(29, 23); this.labelTest.Size = new System.Drawing.Size(106, 13); this.labelTest.Text = "Label avant opération"; this.Controls.Add(this.labelTest); } private void ThreadMethod() { labelTest.Text = "Label après opération" ; } public staticvoid Main() { Application.Run(newMainForm()); } } }
La méthode du Thread (ThreadMethod) se charge seulement de changer le label inclus dans la Form principale.
Vous allez me dire, où se trouve le problème ? Compilé avec le framework.Net 1.x, cet exemple marche parfaitement. En effet, mais il ne s'en trouve pas moins que c'est "techniquement pas beau". Pire encore, compilé avec le Framework 2.0, une exception de type InvalidOperationException est levée.
Que faire pour solutionner ce problème ? Utiliser des délégations synchrones ou asynchrones.
Un appel est dit synchrone lorsque le thread qui a réalisé cet appel attend que la méthode soit terminée avant de reprendre la main. Ceci implique donc un blocage du thread.
Dans le cadre d'un appel asynchrone, la méthode appelée rend la main immédiatement, sans que celle-ci soit forcément terminée.
De plus nous n'avons pas à nous préoccuper de quel thread exécute une méthode. C'est le pool de threads qui gère ceci.
3 méthodes vont nous être utiles pour effectuer ces délégations: Control.Invoke(delegate, object[]) dans le cadre d'une délégation synchrone et le couple Control.BeginInvoke(delegate,object[]) / Control.EndInvoke(IAsyncResult) dans le cadre d'une délégation asynchrone.
A noter que sur le framework 2.0, il existe les méthodes Control.Invoke(delegate) et Control.BeginInvoke(delegate), à utiliser quand on a pas de paramètre a passer.
Pour appeler une méthode de manière synchrone ou asynchrone, il nous suffit de référencer cette méthode dans un délégué ayant la même signature que celle-ci, et d'utiliser une des 2 fonctions citées ci-dessus.
Nous pouvons utiliser Control.Invoke lorsque le délégué ne retourne pas de valeur et surtout quand les instructions invoquées sont courtes, car la méthode Invoke effectue un appel synchrone ce qui veut dire qu'elle est bloquante. Ce "blocage" va se faire au niveau du thread que nous avons créé. Cela implique que toutes les instructions se trouvant après l'appel de la méthode Invoke seront exécutée une fois seulement la méthode appelée par le délégué terminée.
Dans le cas de notre programme de test, nous allons créer une méthode qui se charge simplement de changer la valeur du Label.
private void ChangeLabelText( string str) { labelTest.Text = str; }
Ensuite, créons le délégué correspondant
private delegate void ChangeLabel(string str);
L'utilisation de la méthode Invoke s'effectue ainsi, dans la méthode ThreadMethod
this.Invoke(newChangeLabel(ChangeLabelText), "Label après opération");
using System; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace CrossThreadOp { public class MainForm : Form { private Thread t; private Label labelTest; private delegate void ChangeLabel (string str); public Main Form() { InitializeComponent(); t = new Thread (new ThreadStart (ThreadMethod)); t.Start(); } private void ChangeLabelText(string str) { labelTest.Text = str; //Thread.Sleep(2000); } private void ThreadMethod() { this.Invoke(new ChangeLabel(ChangeLabelText), "Label après opération"); //les instructions se trouvant ici seront appelées //une fois la méthode ChangeLabelText exécutée et terminée. MessageBox.Show("Invoke terminé"); } private void InitializeComponent() { this.labelTest = new System.Windows.Forms.Label(); this.labelTest.AutoSize = true ; this.labelTest.Location = new System.Drawing.Point(29, 23); this.labelTest.Size = new System.Drawing.Size(106, 13); this.labelTest.Text = "Label avant opération" ; this.Controls.Add(this.labelTest); } public static void Main() { Application.Run(new MainForm()); } } }
Pour démontrer que la méthode Invoke est bloquante, il suffit de décommenter l'instruction « Thread.Sleep(2000) ; » se trouvant dans la méthode ChangeLabel. L'instruction MessageBox.Show() suivant l'appel à Invoke sera exécutée après 2 secondes.
Contrairement à la méthode Invoke, la méthode BeginInvoke s'utilise quand on attend une valeur de retour de la part de notre méthode et/ou quand on souhaite que les instructions la suivant soient appelées immédiatement. L'appel de la méthode BeginInvoke s'effectue, rappelons-le, de façon asynchrone.
Pour tester ceci, nous allons simplement effectuer le chargement d'un tableau de string depuis le Thread principal, et retourner le nombre de strings chargées, que l'on affichera dans un MessageBox. (Bon d'accord, cela ne sert à rien vu qu'on connaît dès le départ le nombre de chaines dans le tableau. Mais c'est juste pour l'exemple).
La valeur de retour de l'appel asynchrone peut être récupérée à l'aide de la méthode EndInvoke.
Nous déclarons ici la méthode LoadStringArray(string[]), qui retourne la longueur du tableau passé en paramètre, ainsi que le délégué LoadArray(string[]).
private delegate int LoadArray(string[] array); private int LoadStringArray(string[] array) { return array.Length; }
Ajoutons maintenant ces lignes dans notre méthode ThreadMethod :
string[] values = newstring[]{ "aaa", "bbb","ccc", "ddd"}; IAsyncResult ias = this.BeginInvoke(newLoadArray(LoadStringArray), newobject[] { values}); int count = (int)this.EndInvoke(ias); MessageBox.Show(count.ToString());
Rien de très compliqué. La première ligne crée le tableau de strings que nous passerons en paramètre.
A la seconde ligne, nous voyons que la méthode BeginInvoke retourne un objet de type AsyncResult.
Nous le passons, à la 3eme ligne, en argument a la méthode EndInvoke, pour pouvoir identifier l'appel asynchrone et récupérer le nombre de valeurs chargées.
Pour finir, nous affichons le résultat à la quatrième ligne.
Magique, le MessageBox affiche la valeur 4, il s'agit bien du nombre de strings que le tableau comprend.
Il est important de noter cependant que l'utilisation de la méthode EndInvoke est bloquante. En effet, si nous structurons notre méthode ThreadMethod de cette façon :
private void ThreadMethod() { string[] values = newstring[] { "aaa", "bbb", "ccc", "ddd" }; IAsyncResultias = this.BeginInvoke(newLoadArray(LoadStringArray), newobject[] { values }); int count = (int)this.EndInvoke(ias); MessageBox.Show(count.ToString()); this.Invoke(newChangeLabel(ChangeLabelText), "Label après opération"); }
Nous nous apercevons que le changement de valeur du label se fait seulement après l'appel d'EndInvoke (et du MessageBox.Show).
Notons aussi que même si vous n'attendez pas de valeur en retour quand vous appelez BeginInvoke, il est nécessaire d'appeler la méthode EndInvoke pour chaque BeginInvoke dans votre code. Ceci afin que la CLR nettoie proprement le délégué.
using System; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace CrossThreadOp { public class MainForm : Form { private Thread t; private Label labelTest; public MainForm() { InitializeComponent(); t = new Thread(new ThreadStart (ThreadMethod)); t.Start(); } private delegate void ChangeLabel( string str); private void ChangeLabelText(string str) { labelTest.Text = str; } private delegate int LoadArray(string[] array); private int LoadStringArray(string[] array) { return array.Length; } private void ThreadMethod() { string [] values = new string [] { "aaa" , "bbb" , "ccc" , "ddd" }; IAsyncResult ias = this.BeginInvoke(new LoadArray (LoadStringArray), new object[] { values }); // toutes les instructions se trouvant entre BeginInvoke et EndInvoke seront appelées sans //attendre que la méthode LoadStringArray soit terminée this.Invoke(new ChangeLabel (ChangeLabelText), "Label pendant opération"); int count = ( int ) this.EndInvoke(ias); // Les instructions se trouvant ici s'executeront une fois la méthode EndInvoke terminée MessageBox.Show(count.ToString()); this.Invoke(new ChangeLabel (ChangeLabelText), "Label après opération" ); } private void InitializeComponent() { this.labelTest = new System.Windows.Forms.Label(); this.labelTest.AutoSize = true ; this.labelTest.Location = new System.Drawing.Point(29, 23); this.labelTest.Size = new System.Drawing.Size(106, 13); this.labelTest.Text = "Label avant opération" ; this.Controls.Add(this.labelTest); } public static void Main() { Application.Run(new MainForm()); } } }
Pour remonter une erreur qui se serait déclenchée lors de l'appel d'un délégué asynchrone, il faut appeler la méthode EndInvoke de l'instance du délégué :
Exemple :
public delegate void MonDelegate(); public void MethodeDeleguee() { thrownewException(); } public void CallDelegate() { try { MonDelegate monDel = MethodeDeleguee; IAsyncResult iar = monDel.BeginInvoke(null, null); monDel.EndInvoke(iar); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
Avec ces exemples vous savez maintenant comment utiliser facilement les méthodes Invoke et BeginInvoke.
Vous trouverez ici un exemple utilisant cette technique avec une ProgressBar.
Vous pouvez aussi jeter un oeil à la source de coq, en relation avec le sujet de ce tutoriel.