comment : effectuer des appels thread-safe à des contrôles Windows Forms

le multithreading peut améliorer les performances des applications de Windows Forms, mais l’accès aux contrôles de Windows Forms n’est pas thread-safe par nature. Le multithreading peut exposer votre code à des bogues très sérieux et complexes. Au moins deux threads manipulant un contrôle peuvent forcer le contrôle dans un état incohérent et provoquer des conditions de concurrence, des blocages, des blocages et des blocages. Si vous implémentez le multithreading dans votre application, veillez à appeler les contrôles inter-threads de façon thread-safe. Pour plus d’informations, consultez meilleures pratiques pour le threading managé.

il existe deux façons d’appeler sans risque un contrôle Windows Forms à partir d’un thread qui n’a pas créé ce contrôle. Vous pouvez utiliser la System.Windows.Forms.Control.Invoke méthode pour appeler un délégué créé dans le thread principal, qui à son tour appelle le contrôle. Ou vous pouvez implémenter 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 inter-threads non sécurisés

Il est risqué 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 System.Windows.Forms.TextBox contrôle. Le Button1_Click Gestionnaire d’événements crée un nouveau WriteTextUnsafe thread, qui définit directement la propriété du TextBox.Text thread principal.

private void Button1_Click(object sender, EventArgs e)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(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 threads non sécurisés en déclenchant un InvalidOperationException avec le message, opération inter-threads non valide. Contrôle «» accessible à partir d’un thread autre que le thread sur lequel il a été créé. le se produit toujours pour les appels inter-threads non sécurisés pendant le InvalidOperationException débogage de Visual Studio et peut se produire au moment de l’exécution de l’application. Vous devez résoudre le problème, mais vous pouvez désactiver l’exception en affectant à false la propriété la Control.CheckForIllegalCrossThreadCalls valeur.

Coffre les appels inter-threads

les exemples de code suivants illustrent deux façons d’appeler sans risque un contrôle Windows Forms à partir d’un thread qui n’a pas créé ce dernier :

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

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

vous pouvez générer et exécuter ces exemples comme des applications .NET Framework à partir de la ligne de commande C# ou Visual Basic. Pour plus d’informations, consultez génération à partir de la ligne de commande avec csc.exe ou génération à partir de la ligne de commande (Visual Basic).

à compter de .net core 3,0, vous pouvez également générer et exécuter les exemples comme des applications Windows .net core à partir d’un dossier qui contient un fichier de projet. csproj du Windows Forms < de dossier > .net core.

Exemple : utilisation de la méthode Invoke avec un délégué

l’exemple suivant illustre un modèle permettant de garantir des appels thread-safe à un contrôle Windows Forms. Elle interroge la System.Windows.Forms.Control.InvokeRequired propriété, qui compare l’ID de thread de création du contrôle à l’ID de thread appelant. Si les ID de thread sont identiques, il appelle le contrôle directement. Si les ID de thread sont différents, il appelle la Control.Invoke méthode avec un délégué du thread principal, qui effectue l’appel réel au contrôle.

Le SafeCallDelegate permet de définir la TextBox propriété du Text contrôle. La WriteTextSafe méthode interroge InvokeRequired . Si InvokeRequired retourne true , WriteTextSafe passe SafeCallDelegate à la Invoke méthode pour effectuer l’appel réel au contrôle. Si InvokeRequired retourne false , WriteTextSafe définit TextBox.Text directement. Le Button1_Click Gestionnaire d’événements crée le nouveau thread et exécute la WriteTextSafe méthode.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

Exemple : utilisation d’un gestionnaire d’événements BackgroundWorker

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

Pour effectuer un appel thread-safe à l’aide BackgroundWorker de, créez une méthode dans le thread d’arrière-plan pour effectuer le travail et liez-la à l' DoWork événement. Créez une autre méthode dans le thread principal pour signaler les résultats du travail en arrière-plan et liez-le à l' ProgressChanged événement ou RunWorkerCompleted . Pour démarrer le thread d’arrière-plan, appelez BackgroundWorker.RunWorkerAsync .

L’exemple utilise le RunWorkerCompleted Gestionnaire d’événements pour définir la TextBox propriété du Text contrôle. Pour obtenir un exemple d’utilisation de l' ProgressChanged événement, consultez BackgroundWorker .

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

Voir aussi