Codingly

(InvokeRequired + Invoke) < SynchronizationContext

Posted in Posts by Romain Verdier on août 4, 2008

Tous les développeurs savent qu’il faut être prudent lorsqu’on est en Corée du Nord ou lorsqu’on s’amuse avec les Winforms dans un contexte multi-threads. On ne doit pas, plus précisément, accéder à un contrôle graphique à partir d’un thread différent de celui dans lequel il a été créé. Ceux qui ont essayé (tous, je crois) ont découvert qu’une InvalidOperationException était levée :

Opération inter-threads non valide : le contrôle ‘MyControl’ a fait l’objet d’un accès à partir d’un thread autre que celui sur lequel il a été créé.

Enfin, à partir de VS2005 et en debug mode uniquement. Par exemple, dans une Form le code suivant affecté à l’évènement Click d’un bouton suffit à provoquer une telle exception :

private void myButton_Click(object sender, EventArgs e)
{
    var thread = new Thread((ThreadStart) delegate { this.Text = "Yeah"; });
    thread.Start();
}

L’accès à la propriété Text de la fenêtre (qui est un contrôle) s’effectue dans un thread différent du thread d’affichage principal.

Si je vous demande comment arranger ça, vous allez me répondre naturellement qu’il faut écrire quelque chose comme :

private void myButton_Click(object sender, EventArgs e)
{
    var thread = new Thread(ChangeFormCaption);
    thread.Start();
}

private void ChangeFormCaption()
{
    if (this.InvokeRequired)
    {
        this.Invoke((MethodInvoker) ChangeFormCaption);
    }
    else
    {
        this.Text = "Yeah";
    }
}

On teste via la propriété InvokeRequired si on est dans le thread ayant créé la fenêtre. Si ce n’est pas le cas, alors on utilise la méthode Invoke pour exécuter le délégué passé en paramètre (ChangeFormCaption) dans le bon thread : celui à partir duquel la fenêtre a été créée.

Ca fonctionne, hourra.

Maintenant, un autre exemple :

public partial class MyForm : Form
{
    public MyForm()
    {
        this.InitializeComponent();

        // New background thread started from the form constructor
        var thread = new Thread(DoSomething);
        thread.Start();

        Thread.Sleep(100);
    }

    public void DoSomething()
    {
        if (this.InvokeRequired)
        {
            this.Invoke((ThreadStart) this.DoSomething);
        }
        else
        {
            Thread.Sleep(100);

            // Access to the control (the form itself, actually)
            // from the background thread
            this.Text = "Yeah";
        }
    }
}

A priori, ça fonctionne, puisqu’on prend bien garde d’utiliser la méthode Invoke lorsque nécessaire. Mais en fait – et c’est bien là tout l’intérêt de ce billet si tant est qu’il y en ait un – ça ne fonctionne pas toujours. On continue d’obtenir plus ou moins aléatoirement l’InvalidOperationException liée à l’interaction cross-thread condamnée plus tôt.

Mais pourquoi ?

Un nord-coréen répondrait sans doute "parce que", mais je ne suis pas nord-coréen. La MSDN non plus, faut croire, puisqu’après avoir halluciné quelque temps j’y ai trouvé une ligne spécifiant le comportement de la propriété InvokeRequired lorsque le handle du contrôle sur lequel elle est appelée n’est pas encore créé : elle retourne false.

C’est fâcheux, et c’est aussi ce qu’illustre l’exemple précédent. Dans certains cas, en lançant le background thread durant la construction de la fenêtre, le handle de cette dernière peut ne pas avoir été créé lors de l’accès à la propriété InvokeRequired. Cela entraine logiquement la modification de la fenêtre directement à partir du background thread, et l’exception est levée. Notez qu’effectuer la modification en appelant systématiquement la méthode Invoke n’arrange rien : cette dernière nécessite également que le handle du contrôle soit créé, et lèvera une autre InvalidOperationException si ce n’est pas le cas.

Le SynchronizationContext permet de régler ça. En fait, c’est une solution disponible à partir du framework 2.0 qui peut complètement remplacer le pattern if(InvokeRequired) Invoke(...), et qui gère les handles correctement. C’est ce qu’utilise le BackgroundWorker, pour info. Voilà ce que ça donne à l’utilisation :

public partial class MyForm : Form
{
    private readonly SynchronizationContext syncContext;

    public MyForm()
    {
        this.InitializeComponent();

        // Getting the synchronization context from the
        // form constructor
        this.syncContext = SynchronizationContext.Current;

        // New background thread started from the form constructor
        var thread = new Thread(this.DoSomething);
        thread.Start();

        Thread.Sleep(100);
    }

    public void DoSomething()
    {
        this.syncContext.Post(delegate
                              {
                                  Thread.Sleep(100);

                                  // Access to the control from the 
                                  // synchronization context
                                  this.Text = "Yeah";

                              }, null);
    }
}

Lors de la construction de la fenêtre, avant tout, on récupère le contexte de synchronisation correspondant au thread courant, et on conserve sa référence. Dans le background thread (méthode DoSomething) on accède à la fenêtre via la méthode Post du SynchronizationContext. Et voilà.

Je vous laisse lire la documentation pour en apprendre plus, car il y a plus à apprendre. Je voulais surtout mettre en avant aujourd’hui le comportement éventuellement troublant de la propriété InvokeRequired, qui peut facilement faire perdre du temps dans ce scénario bien particulier.

About these ads
Tagged with: , ,

5 Réponses

Subscribe to comments with RSS.

  1. Skalp said, on août 4, 2008 at 9:48  

    Rhaaa, je savais bien que je n’étais pas le seul à avoir rencontré ce problème :
    [...] après avoir halluciné quelque temps j’y ai trouvé une ligne spécifiant le comportement de la propriété InvokeRequired lorsque le handle du contrôle sur lequel elle est appelée n’est pas encore créé : elle retourne false.
    Après avoir passé et repassé le code au debug pas à pas, j’ai également fini par trouver cette information sur la MSDN. Mais lorsque je suis parvenu jusqu’au SynchronizeContext, j’ai lâché l’affaire car j’ai trouvé les explications trop obscures pour moi…
    Je vais tester l’astuce asap (à mon retour de congés ^^), merci !

  2. grozeille said, on août 5, 2008 at 10:18  

    Ça c’est cool comme tips!! Et ça fait un petit rappelle sur les dangers des threads en Winforms.

  3. Manitra said, on mai 13, 2009 at 2:33  

    Bonjour,

    Merci beaucoup pour cette astuce, la syntaxe est beaucoup plus simple et ça fonctionne.

    Pour info, pour éviter ce bug, je ne faisait jamais rien dans mes contructeurs de winform, je faisais tout dans le OnLoad.

    Manitra.

  4. Waulslepauls said, on juillet 20, 2009 at 6:43  

    Dans le post il dit que ça fonctionne pas toujours si tu "lances le thread dans le constructeur de la Form", sauf que dans le constructeur de la Form t’es pas sensé lancer de threads….

    Imagine tu fais :

    Form f = new Form() ;
    => Ta pas encore affiché ta Form que déjà ta lancé des threads….. je vois pas bien à quoi ça sert

    Normalement si tu veux lancer des threads a l’ouverture la Form tu les mets dans le Form_Load avec l’allocation des ressources utilisées par le formulaire.

  5. David said, on juillet 4, 2011 at 4:07  

    Excellente démarche d’explication et recherche


Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Suivre

Recevez les nouvelles publications par mail.

%d bloggers like this: