[c#] opérations cross threads - utilisation des delegations synchrones / asynchrones

Opérations cross-threads - utilisation des délégations synchrones / asynchrones

Introduction

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.

Définitions synchrone / asynchrone

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.

La méthode Control.Invoke

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

Listing complet du programme modifié

 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.

Utilisation de la méthode Control.BeginInvoke

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é.

Listing complet du programme modifié

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

Info : Remonter une erreur qui se produit dans le délégué asynchrone

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.

A voir également
Ce document intitulé « [c#] opérations cross threads - utilisation des delegations synchrones / asynchrones » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous