Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
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 button2_Click(object sender, EventArgs e)
{
WriteTextUnsafe("Writing message #1 (UI THREAD)");
_ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}
private void WriteTextUnsafe(string text) =>
textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
WriteTextUnsafe("Writing message #1 (UI THREAD)")
Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub
Private Sub WriteTextUnsafe(text As String)
TextBox1.Text += $"{Environment.NewLine}{text}"
End Sub
Le débogueur Visual Studio détecte ces appels de thread non sécurisés en générant un InvalidOperationException message, l’opération interthread n’est pas valide. Contrôle accessible à partir d’un thread autre que le thread sur lequel il a été créé. Le InvalidOperationException problème se produit toujours pour les appels interthread non sécurisés pendant le 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 définissant la propriété Control.CheckForIllegalCrossThreadCalls sur false.
Appels inter-threads sécurisés
Les applications Windows Forms suivent une infrastructure stricte de type contrat, similaire à toutes les autres infrastructures d’interface utilisateur Windows : tous les contrôles doivent être créés et accessibles à partir du même thread. Cela est important, car Windows exige que les applications fournissent un thread dédié unique pour transmettre des messages système. Chaque fois que le Gestionnaire de fenêtres Windows détecte une interaction avec une fenêtre d’application, tel qu’une touche, un clic de souris ou un redimensionnement de la fenêtre, il route ces informations vers le thread qui a créé et gère l’interface utilisateur, et le transforme en événements actionnables. Ce thread est appelé thread d’interface utilisateur.
Étant donné que le code s’exécutant sur un autre thread ne peut pas accéder aux contrôles créés et gérés par le thread d’interface utilisateur, Windows Forms fournit des moyens de travailler en toute sécurité avec ces contrôles à partir d’un autre thread, comme illustré dans les exemples de code suivants :
Exemple : Utiliser Control.InvokeAsync (.NET 9 et versions ultérieures)
Méthode Control.InvokeAsync (.NET 9+), qui fournit un marshaling asynchrone au thread d’interface utilisateur.
Exemple : Utilisez la méthode Control.Invoke :
Méthode Control.Invoke, qui appelle un délégué du thread principal pour appeler le contrôle.
Exemple : Utiliser un BackgroundWorker
Un composant BackgroundWorker, qui offre un modèle piloté par les événements.
Exemple : Utiliser Control.InvokeAsync (.NET 9 et versions ultérieures)
À compter de .NET 9, Windows Forms inclut la InvokeAsync méthode, qui fournit un marshaling asynchrone vers le thread d’interface utilisateur. Cette méthode est utile pour les gestionnaires d’événements asynchrones et élimine de nombreux scénarios d’interblocage courants.
Note
Control.InvokeAsync est disponible uniquement dans .NET 9 et versions ultérieures. Il n’est pas pris en charge dans .NET Framework.
Compréhension de la différence : Invoke vs InvokeAsync
Control.Invoke (Envoi - Blocage) :
- Envoie de façon synchrone le délégué à la file d’attente de messages du thread d’interface utilisateur.
- Le thread appelant attend que le thread d’interface utilisateur traite le délégué.
- Peut entraîner le blocage de l’interface utilisateur lorsque le délégué marshalé vers la file d’attente de messages attend qu’un message arrive (blocage).
- Utile lorsque vous disposez de résultats prêts à s’afficher sur le thread d’interface utilisateur, par exemple : désactivation d’un bouton ou définition du texte d’un contrôle.
Control.InvokeAsync (publication - Non bloquant) :
- Publie de façon asynchrone le délégué dans la file d’attente de messages du thread d’interface utilisateur au lieu d’attendre la fin de l’appel.
- Retourne immédiatement sans bloquer le thread appelant.
- Retourne une
Taskvaleur qui peut être attendue pour l’achèvement. - Idéal pour les scénarios asynchrones et empêche les goulots d’étranglement des threads d’interface utilisateur.
Avantages d’InvokeAsync
Control.InvokeAsync présente plusieurs avantages par rapport à l’ancienne Control.Invoke méthode. Elle retourne une Task valeur que vous pouvez attendre, ce qui le rend efficace avec le code asynchrone et await. Il empêche également les problèmes courants de blocage qui peuvent se produire lors du mélange de code asynchrone avec des appels d’appel synchrones. Contrairement Control.Invokeà , la InvokeAsync méthode ne bloque pas le thread appelant, ce qui maintient vos applications réactives.
La méthode prend en charge l’annulation via CancellationToken, de sorte que vous pouvez annuler les opérations si nécessaire. Il gère également les exceptions correctement, en les renvoyant à votre code afin de pouvoir traiter les erreurs de manière appropriée. .NET 9 inclut des avertissements du compilateur (WFO2001) qui vous aident à utiliser la méthode correctement.
Pour obtenir des conseils complets sur les gestionnaires d’événements asynchrones et les meilleures pratiques, consultez vue d’ensemble des événements.
Choix de la surcharge InvokeAsync appropriée
Control.InvokeAsync fournit quatre surcharges pour différents scénarios :
| Surcharger | Cas d’usage | Example |
|---|---|---|
InvokeAsync(Action) |
Opération de synchronisation, aucune valeur de retour. | Mettez à jour les propriétés du contrôle. |
InvokeAsync<T>(Func<T>) |
Opération de synchronisation, avec valeur de retour. | Obtenir l’état du contrôle. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Opération asynchrone, aucune valeur de retour.* | Mises à jour de l’interface utilisateur de longue durée. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Opération asynchrone, avec valeur de retour.* | Extraction de données asynchrones avec résultat. |
*Visual Basic ne prend pas en charge l’attente d’un ValueTask.
L’exemple suivant montre comment mettre InvokeAsync à jour les contrôles en toute sécurité à partir d’un thread d’arrière-plan :
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Perform background work
await Task.Run(async () =>
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate work
await Task.Delay(100);
// Create local variable to avoid closure issues
int currentProgress = i;
// Update UI safely from background thread
await loggingTextBox.InvokeAsync(() =>
{
loggingTextBox.Text = $"Progress: {currentProgress}%";
});
}
});
loggingTextBox.Text = "Operation completed!";
}
finally
{
button1.Enabled = true;
}
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
button1.Enabled = False
Try
' Perform background work
Await Task.Run(Async Function()
For i As Integer = 0 To 100 Step 10
' Simulate work
Await Task.Delay(100)
' Create local variable to avoid closure issues
Dim currentProgress As Integer = i
' Update UI safely from background thread
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = $"Progress: {currentProgress}%"
End Sub)
Next
End Function)
' Update UI after completion
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = "Operation completed!"
End Sub)
Finally
button1.Enabled = True
End Try
End Sub
Pour les opérations asynchrones qui doivent s’exécuter sur le thread d’interface utilisateur, utilisez la surcharge asynchrone :
private async void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
try
{
loggingTextBox.Text = "Starting operation...";
// Dispatch and run on a new thread, but wait for tasks to finish
// Exceptions are rethrown here, because await is used
await Task.WhenAll(Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync));
// Dispatch and run on a new thread, but don't wait for task to finish
// Exceptions are not rethrown here, because await is not used
_ = Task.Run(SomeApiCallAsync);
}
catch (OperationCanceledException)
{
loggingTextBox.Text += "Operation canceled.";
}
catch (Exception ex)
{
loggingTextBox.Text += $"Error: {ex.Message}";
}
finally
{
button2.Enabled = true;
}
}
private async Task SomeApiCallAsync()
{
using var client = new HttpClient();
// Simulate random network delay
await Task.Delay(Random.Shared.Next(500, 2500));
// Do I/O asynchronously
string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
// Marshal back to UI thread
await this.InvokeAsync(async (cancelToken) =>
{
loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
});
// Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
button2.Enabled = False
Try
loggingTextBox.Text = "Starting operation..."
' Dispatch and run on a new thread, but wait for tasks to finish
' Exceptions are rethrown here, because await is used
Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync))
' Dispatch and run on a new thread, but don't wait for task to finish
' Exceptions are not rethrown here, because await is not used
Call Task.Run(AddressOf SomeApiCallAsync)
Catch ex As OperationCanceledException
loggingTextBox.Text += "Operation canceled."
Catch ex As Exception
loggingTextBox.Text += $"Error: {ex.Message}"
Finally
button2.Enabled = True
End Try
End Sub
Private Async Function SomeApiCallAsync() As Task
Using client As New HttpClient()
' Simulate random network delay
Await Task.Delay(Random.Shared.Next(500, 2500))
' Do I/O asynchronously
Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
' Marshal back to UI thread
' Extra work here in VB to handle ValueTask conversion
Await Me.InvokeAsync(DirectCast(
Async Function(cancelToken As CancellationToken) As Task
loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
End Function,
Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
)
' Do more Async I/O ...
End Using
End Function
Note
Si vous utilisez Visual Basic, l’extrait de code précédent a utilisé une méthode d’extension pour convertir un ValueTaskTasken . Le code de méthode d’extension est disponible sur GitHub.
Exemple : Utiliser la méthode Control.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)
{
WriteTextSafe("Writing message #1");
_ = Task.Run(() => WriteTextSafe("Writing message #2"));
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));
else
textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WriteTextSafe("Writing message #1")
Task.Run(Sub() WriteTextSafe("Writing message #2"))
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (NON-UI THREAD)")
End Sub)
Else
TextBox1.Text += $"{Environment.NewLine}{text}"
End If
End Sub
Pour plus d’informations sur la façon dont Invoke diffère la différence, consultez Présentation de InvokeAsyncla différence : Invoke vs InvokeAsync.
Exemple : Utiliser un BackgroundWorker
Un moyen simple d’implémenter des scénarios de multithreading tout en garantissant que l’accès à un contrôle ou un formulaire est effectué uniquement sur le thread principal (thread d’interface utilisateur), est avec le System.ComponentModel.BackgroundWorker composant, 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.
Important
Le BackgroundWorker composant n’est plus l’approche recommandée pour les scénarios asynchrones dans les applications Windows Forms. Bien que nous continuions à prendre en charge ce composant pour la compatibilité descendante, il traite uniquement du déchargement de la charge de travail du processeur du thread d’interface utilisateur vers un autre thread. Il ne gère pas d’autres scénarios asynchrones tels que les E/S de fichier ou les opérations réseau où le processeur peut ne pas fonctionner activement.
Pour la programmation asynchrone moderne, utilisez async plutôt des méthodes await . Si vous devez décharger explicitement le travail gourmand en charge du processeur, utilisez Task.Run pour créer et démarrer une tâche, que vous pouvez alors attendre comme toute autre opération asynchrone. Pour plus d’informations, consultez Exemple : Utiliser Control.InvokeAsync (.NET 9 et versions ultérieures) et les opérations et événements entre threads.
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 est terminé. 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(); // Not awaitable
}
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() ' Not awaitable
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
.NET Desktop feedback