Partager via


Comment effectuer des appels thread-safe aux contrôles

Le multithreading peut améliorer les performances des applications Windows Forms, mais l’accès aux contrôles Windows Forms n’est pas intrinsèquement thread-safe. Le multithreading peut exposer votre code à des bogues sérieux et complexes. Deux threads ou plus qui manipulent un contrôle peuvent forcer le contrôle à entrer dans un état incohérent et entraîner des conditions de concurrence, des interblocages et des blocages. Si vous implémentez le multithreading dans votre application, veillez à appeler les contrôles entre threads de manière sûre. Pour plus d’informations, consultez Meilleures pratiques pour le threading managé.

Il existe deux façons d’appeler en toute sécurité un contrôle Windows Forms à partir d’un thread qui n’a pas créé ce contrôle. Utilisez la méthode System.Windows.Forms.Control.Invoke pour appeler un délégué créé dans le thread principal, qui appelle à son tour le contrôle. Ou, implémentez un System.ComponentModel.BackgroundWorker, qui utilise un modèle piloté par les événements pour séparer le travail effectué dans le thread d’arrière-plan de la création de rapports sur les résultats.

Appels interthread non sécurisés

Il est dangereux d’appeler un contrôle directement à partir d’un thread qui ne l’a pas créé. L’extrait de code suivant illustre un appel non sécurisé au contrôle System.Windows.Forms.TextBox. Le gestionnaire d’événements Button1_Click crée un thread WriteTextUnsafe, qui définit directement la propriété TextBox.Text du thread principal.

private void button1_Click(object sender, EventArgs e)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
    thread2.Start()
End Sub

Private Sub WriteTextUnsafe()
    TextBox1.Text = "This text was set unsafely."
End Sub

Le débogueur Visual Studio détecte ces appels de thread non sécurisés en générant un InvalidOperationException avec le message opération interthread non valide. Contrôle accédé depuis un thread autre que celui sur lequel il a été créé. Le InvalidOperationException se produit toujours pour les appels interthreads non sécurisés lors du débogage de Visual Studio et peut également survenir au moment de l'exécution de l'application. Vous devez résoudre le problème, mais vous pouvez désactiver l’exception en définissant la propriété Control.CheckForIllegalCrossThreadCalls sur false.

Appels inter-threads sécurisés

Les exemples de code suivants montrent deux façons d’appeler en toute sécurité un contrôle Windows Forms à partir d’un thread qui ne l’a pas créé :

  1. Méthode System.Windows.Forms.Control.Invoke, qui appelle un délégué du thread principal pour appeler le contrôle.
  2. Un composant System.ComponentModel.BackgroundWorker, qui offre un modèle piloté par les événements.

Dans les deux exemples, le thread d’arrière-plan veille pendant une seconde pour simuler le travail effectué dans ce thread.

Exemple : Utiliser la méthode Invoke

L’exemple suivant illustre un modèle permettant de garantir des appels thread-safe vers un contrôle Windows Forms. Il interroge la propriété System.Windows.Forms.Control.InvokeRequired, qui compare l’ID de thread de création du contrôle à l’ID de thread appelant. S’ils sont différents, vous devez appeler la méthode Control.Invoke.

La WriteTextSafe permet de définir la propriété TextBox du contrôle Text à une nouvelle valeur. La méthode interroge InvokeRequired. Si InvokeRequired retourne true, WriteTextSafe s’appelle de manière récursive, en passant la méthode en tant que délégué à la méthode Invoke. Si InvokeRequired retourne false, WriteTextSafe définit le TextBox.Text directement. Le gestionnaire d’événements Button1_Click crée le nouveau thread et exécute la méthode WriteTextSafe.

private void button1_Click(object sender, EventArgs e)
{
    var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
    var thread2 = new System.Threading.Thread(threadParameters);
    thread2.Start();
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
    {
        // Call this same method but append THREAD2 to the text
        Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
        textBox1.Invoke(safeWrite);
    }
    else
        textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

Exemple : Utiliser un BackgroundWorker

Un moyen simple d’implémenter le multithreading est avec le composant System.ComponentModel.BackgroundWorker, qui utilise un modèle piloté par les événements. Le thread d’arrière-plan déclenche l’événement BackgroundWorker.DoWork, qui n’interagit pas avec le thread principal. Le thread principal exécute les gestionnaires d’événements BackgroundWorker.ProgressChanged et BackgroundWorker.RunWorkerCompleted, qui peuvent appeler les contrôles du thread principal.

Pour effectuer un appel thread-safe à l’aide de BackgroundWorker, gérez l’événement DoWork. Il existe deux événements que le collaborateur en arrière-plan utilise pour signaler l’état : ProgressChanged et RunWorkerCompleted. L’événement ProgressChanged est utilisé pour communiquer les mises à jour d’état au thread principal, et l’événement RunWorkerCompleted est utilisé pour signaler que le worker en arrière-plan a terminé son travail. Pour démarrer le thread d’arrière-plan, appelez BackgroundWorker.RunWorkerAsync.

L’exemple compte de 0 à 10 dans l’événement DoWork, en s'arrêtant pendant une seconde entre chaque nombre. Il utilise le gestionnaire d’événements ProgressChanged pour signaler le nombre au thread principal et définir la propriété TextBox du contrôle Text. Pour que l’événement ProgressChanged fonctionne, la propriété BackgroundWorker.WorkerReportsProgress doit être définie sur true.

private void button1_Click(object sender, EventArgs e)
{
    if (!backgroundWorker1.IsBusy)
        backgroundWorker1.RunWorkerAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    int counter = 0;
    int max = 10;

    while (counter <= max)
    {
        backgroundWorker1.ReportProgress(0, counter.ToString());
        System.Threading.Thread.Sleep(1000);
        counter++;
    }
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
    textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If (Not BackgroundWorker1.IsBusy) Then
        BackgroundWorker1.RunWorkerAsync()
    End If

End Sub

Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim counter = 0
    Dim max = 10

    While counter <= max

        BackgroundWorker1.ReportProgress(0, counter.ToString())
        System.Threading.Thread.Sleep(1000)

        counter += 1

    End While

End Sub

Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    TextBox1.Text = e.UserState
End Sub