Guide pratique pour effectuer des appels thread-safe aux contrôles Windows Forms
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 très graves et complexes. Deux threads ou plus qui manipulent un contrôle peuvent amener le contrôle à un état incohérent et entraîner des conditions de concurrence, des interblocages et des congélations ou des blocages. Si vous implémentez le multithreading dans votre application, veillez à appeler des contrôles interthread de manière sécurisée. Pour plus d’informations, consultez les meilleures pratiques de gestion des threads.
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. Vous pouvez utiliser 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. Vous pouvez également 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 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)
{
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 'thread' non sécurisés en générant un InvalidOperationException avec le message opération inter-thread 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 inter-thread non sécurisés lors du débogage dans Visual Studio et peut se produire à 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 sécurisés entre threads
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éé :
- Méthode System.Windows.Forms.Control.Invoke, qui appelle un délégué du thread principal pour appeler le contrôle.
- 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.
Vous pouvez générer et exécuter ces exemples en tant qu’applications .NET Framework à partir de la ligne de commande C# ou Visual Basic. Pour plus d’informations, consultez construction de ligne de commande avec csc.exe ou Construire à partir de la ligne de commande (Visual Basic).
À partir de .NET Core 3.0, vous pouvez également compiler et exécuter les exemples en tant qu’applications Windows .NET Core depuis un dossier nommé Windows Forms .NET Core <>, contenant un fichier projet .csproj.
Exemple : Utiliser 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. 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. Si les ID de thread sont identiques, il appelle directement le contrôle. Si les ID de thread sont différents, il appelle la méthode Control.Invoke avec un délégué du thread principal, ce qui effectue l’appel réel au contrôle.
La SafeCallDelegate
active la possibilité de définir la propriété Text du contrôle TextBox. La méthode WriteTextSafe
interroge InvokeRequired. Si InvokeRequired renvoie true
, WriteTextSafe
passe la SafeCallDelegate
à la méthode Invoke pour faire l'appel réel au contrôle. 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
.
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 : Utiliser un gestionnaire d’événements 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 exécute 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, créez une méthode dans le thread d’arrière-plan pour effectuer le travail et liez-la à l’événement DoWork. Créez une autre méthode dans le thread principal pour signaler les résultats du travail en arrière-plan et liez-la à l’événement ProgressChanged ou RunWorkerCompleted. Pour démarrer le thread d’arrière-plan, appelez BackgroundWorker.RunWorkerAsync.
L’exemple utilise le gestionnaire d’événements RunWorkerCompleted pour définir la propriété Text du contrôle TextBox. Pour obtenir un exemple utilisant l’événement ProgressChanged, 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
.NET Desktop feedback