Modèle de thread
Windows Presentation Foundation (WPF) a été conçu pour épargner aux développeurs les difficultés liées aux threads. La majorité des développeurs WPF n'aura par conséquent pas à écrire une interface qui utilise plusieurs threads. Comme les programmes multithread sont complexes et difficiles à déboguer, ils doivent être évités lorsque des solutions monothread existent.
Même si son architecture est bien élaborée, aucune infrastructure d'UI ne sera toutefois jamais en mesure de fournir une solution monothread pour chaque type de problème. WPF s'en rapproche, mais il existe encore des situations où plusieurs threads améliorent la réactivité de l'user interface (UI) ou les performances de l'application.
Cette rubrique comprend les sections suivantes.
- Vue d'ensemble et répartiteur
- Threads en action : exemples
- Détails techniques et points d'entrave
- Rubriques connexes
Vue d'ensemble et répartiteur
En règle générale, les applications WPF commencent avec deux threads : un pour gérer le rendu et un autre pour gérer l'UI. Le thread de rendu s'exécute efficacement masqué en arrière-plan pendant que le thread d'UI reçoit l'entrée, gère les événements, peint l'écran et exécute le code de l'application. La plupart des applications utilisent un seul thread d'UI, même s'il est préférable d'en utiliser plusieurs dans certaines situations. Nous reviendrons ultérieurement sur ce point avec un exemple.
Le thread d'UI met en file d'attente des éléments de travail à l'intérieur d'un objet appelé un Dispatcher. Le Dispatcher sélectionne les éléments de travail en fonction de leur priorité et exécute complètement chacun d'eux. Chaque thread d'UI doit avoir au moins un Dispatcher et chaque Dispatcher peut exécuter les éléments de travail dans précisément un thread.
L'astuce pour générer des applications conviviales et réactives consiste à accroître le débit Dispatcher en gardant la petite taille des éléments de travail. De cette façon, les éléments ne sont jamais périmés dans la file d'attente de traitement Dispatcher. Tout délai perceptible entre entrée et réponse peut frustrer un utilisateur.
Comment les applications WPF sont-elles alors supposées gérer les opérations volumineuses ? Que faire si votre code implique un grand calcul ou doit interroger une base de données sur un serveur distant ? Habituellement, la solution consiste à gérer l'opération volumineuse dans un thread séparé, en laissant le thread d'UI libre pour gérer les éléments dans la file d'attente Dispatcher. Lorsque l'opération volumineuse est terminée, il peut indiquer son résultat au thread d'UI et l'afficher.
Historiquement, Windows permet d'accéder à des éléments d'UI uniquement par le thread qui les a créés. En d'autres termes, un thread d'arrière-plan chargé d'effectuer une tâche à durée d'exécution longue ne peut pas mettre à jour une zone de texte lorsqu'il est terminé. Windows se comporte de cette manière afin de garantir l'intégrité des composants de l'UI. Une zone de liste pourrait sembler étrange si son contenu était mis à jour par un thread d'arrière-plan au cours de la peinture.
WPF intègre un mécanisme d'exclusion mutuelle qui applique cette coordination. La plupart des classes de WPF dérivent de DispatcherObject. Au moment de la construction, un DispatcherObject stocke une référence au Dispatcher lié au thread en cours d'exécution. En fait, le DispatcherObject s'associe au thread qui le crée. Pendant l'exécution du programme, un DispatcherObjectpeut appeler sa méthode VerifyAccess publique. VerifyAccess examine le Dispatcher associé au thread actuel et le compare à la référence Dispatcher stockée pendant la construction. S'ils ne correspondent pas,VerifyAccess lève une exception. VerifyAccess est destiné à être appelé au début de chaque méthode qui appartient à un DispatcherObject
Si un seul thread peut modifier l'UI, comment les threads d'arrière-plan interagissent-ils avec l'utilisateur ? Un thread d'arrière-plan peut demander au thread d'UI d'effectuer une opération en son nom. Pour ce faire, il enregistre un élément de travail avec le Dispatcher du thread d'UI. La classe Dispatcher fournit deux méthodes d'enregistrement d'éléments de travail : Invoke et BeginInvoke. Ces deux méthodes planifient un délégué pour l'exécution. Invoke est un appel synchrone, c'est-à-dire qu'il ne retourne pas de valeur tant que le thread d'UI n'a pas terminé d'exécuter le délégué. BeginInvoke est asynchrone et retourne immédiatement une valeur.
Le Dispatcher classe les éléments dans sa file d'attente par priorité. Il y a dix niveaux qui peuvent être spécifiés quand vous ajoutez un élément à la file d'attente Dispatcher. Ces priorités sont maintenues dans l'énumération DispatcherPriority. Les informations détaillées sur les niveaux DispatcherPriority se trouvent dans la documentation Windows SDK.
Threads en action : exemples
Application monothread avec calcul de longue durée
La majorité des graphical user interfaces (GUIs) sont la plupart du temps inactives pendant la génération d'événements en réponse à des interventions de l'utilisateur. En faisant preuve de prudence lors de la programmation, cette durée d'inactivité peut être utilisée de manière constructive, sans affecter la réactivité de l'UI. Le modèle de thread WPF ne permet pas à l'entrée d'interrompre une opération qui se produit dans le thread d'UI. En d'autres termes, vous devez être sûr de revenir périodiquement au Dispatcher pour traiter les événements d'entrée en attente avant qu'ils ne deviennent périmés.
Prenons l'exemple suivant :
Cette application simple compte vers le haut à partir de trois, en recherchant des nombres premiers. Lorsque l'utilisateur clique sur le bouton Démarrer, la recherche commence. Lorsque le programme trouve un nombre premier, il met à jour l'interface utilisateur avec sa découverte. L'utilisateur peut arrêter la recherche à tout moment.
Bien qu'assez simple, la recherche de nombres premiers peut être illimitée, ce qui présente des difficultés. Si nous avions géré l'intégralité de la recherche dans le gestionnaire d'événements Click du bouton, nous n'aurions jamais permis au thread d'UI de gérer d'autres événements. L'UI serait dans l'incapacité de répondre à l'entrée ou de traiter les messages. Elle ne se redessinerait jamais et ne répondrait jamais aux clics sur le bouton.
Nous pourrions mener la recherche de nombre premier dans un thread séparé, mais nous devrions résoudre les problèmes de synchronisation. Avec une approche monothread, nous pouvons mettre à jour directement l'étiquette qui répertorie le plus grand nombre premier trouvé.
Si nous divisons la tâche de calcul en segments maniables, nous pouvons périodiquement retourner au Dispatcher et traiter les événements. Nous pouvons donner une possibilité à WPF de redessiner et traiter l'entrée.
La meilleure façon de fractionner le traitement entre calcul et gestion des événements consiste à gérer le calcul à partir du Dispatcher. Grâce à la méthode BeginInvoke, nous pouvons planifier des contrôles de nombre premier dans la même file d'attente que celle à partir de laquelle les événements de l'UI sont tirés. Dans notre exemple, nous ne planifions qu'un seul contrôle de nombre premier à la fois. Une fois le contrôle de nombre premier effectué, nous planifions immédiatement le contrôle suivant. Ce contrôle reprend une fois seulement que les événements de l'UI en attente ont été traités.
Microsoft Word passe le correcteur orthographique à l'aide de ce mécanisme. La correction orthographique est effectuée en arrière-plan grâce à la durée d'inactivité du thread d'UI. Observons le code.
L'exemple suivant affiche le XAML qui crée l'interface utilisateur.
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="260" Height="75"
>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
<Button Content="Start"
Click="StartOrStop"
Name="startStopButton"
Margin="5,0,5,0"
/>
<TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
</StackPanel>
</Window>
<Window x:Class="SDKSamples.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="260" Height="75"
>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
<Button Content="Start"
Click="StartOrStop"
Name="startStopButton"
Margin="5,0,5,0"
/>
<TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
</StackPanel>
</Window>
Le code suivant montre le code-behind.
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class MainWindow
Inherits Window
Public Delegate Sub NextPrimeDelegate()
'Current number to check
Private num As Long = 3
Private continueCalculating As Boolean = False
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
If continueCalculating Then
continueCalculating = False
startStopButton.Content = "Resume"
Else
continueCalculating = True
startStopButton.Content = "Stop"
startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
End If
End Sub
Public Sub CheckNextNumber()
' Reset flag.
NotAPrime = False
For i As Long = 3 To Math.Sqrt(num)
If num Mod i = 0 Then
' Set not a prime flag to true.
NotAPrime = True
Exit For
End If
Next
' If a prime number.
If Not NotAPrime Then
bigPrime.Text = num.ToString()
End If
num += 2
If continueCalculating Then
startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
End If
End Sub
Private NotAPrime As Boolean = False
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public delegate void NextPrimeDelegate();
//Current number to check
private long num = 3;
private bool continueCalculating = false;
public Window1() : base()
{
InitializeComponent();
}
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to true.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
}
}
L'exemple suivant montre un gestionnaire d'événements pour le Button.
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
If continueCalculating Then
continueCalculating = False
startStopButton.Content = "Resume"
Else
continueCalculating = True
startStopButton.Content = "Stop"
startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
End If
End Sub
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
Excepté mettre à jour le texte sur le Button, ce gestionnaire est chargé de planifier le premier contrôle de nombre premier en ajoutant un délégué à la file d'attente Dispatcher. Après que ce gestionnaire d'événements a terminé son travail, le Dispatcher sélectionne ce délégué pour l'exécution.
Comme nous l'avons mentionné précédemment, BeginInvoke est le membre Dispatcher utilisé pour planifier un délégué pour l'exécution. Dans ce cas, nous choisissons la priorité SystemIdle. Le Dispatcher exécutera ce délégué uniquement s'il n'y a pas d'événements importants à traiter. La réactivité de l'UI est plus importante que le contrôle de nombre. Nous passons également un nouveau délégué qui représente la routine de contrôle de nombre.
Public Sub CheckNextNumber()
' Reset flag.
NotAPrime = False
For i As Long = 3 To Math.Sqrt(num)
If num Mod i = 0 Then
' Set not a prime flag to true.
NotAPrime = True
Exit For
End If
Next
' If a prime number.
If Not NotAPrime Then
bigPrime.Text = num.ToString()
End If
num += 2
If continueCalculating Then
startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
End If
End Sub
Private NotAPrime As Boolean = False
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to true.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
Cette méthode vérifie si le nombre impair suivant est un nombre premier. S'il s'agit d'un nombre premier, la méthode met à jour directement le bigPrimeTextBlock pour refléter sa découverte. Nous pouvons faire ceci parce que le calcul se produit dans le même thread que celui qui a été utilisé pour créer le composant. Si nous avions choisi d'utiliser un thread séparé pour le calcul, nous aurions dû utiliser un mécanisme de synchronisation plus compliqué et exécuter la mise à jour dans le thread d'UI. Nous illustrerons cette situation par la suite.
Pour obtenir le code source complet de cet exemple, consultez Single-Threaded Application with Long-Running Calculation Sample (page éventuellement en anglais)
Gestion d'une opération bloquante avec un thread d'arrière-plan
La gestion d'opérations bloquantes dans une application graphique peut être difficile. Nous ne souhaitons pas appeler des méthodes bloquantes de gestionnaires d'événements parce que l'application apparaîtra figée. Nous pouvons utiliser un thread séparé pour gérer ces opérations, mais lorsque nous avons terminé, nous devons effectuer une synchronisation avec le thread d'UI, car nous ne pouvons pas modifier directement l'GUI de notre thread de travail. Nous pouvons utiliser Invoke ouBeginInvoke pour insérer des délégués dans le Dispatcher du thread d'UI. Finalement, ces délégués seront exécutés avec l'autorisation de modifier des éléments de l'UI.
Dans cet exemple, nous reproduisons un appel de procédure distante qui récupère une prévision météorologique. Nous utilisons un thread de travail séparé pour exécuter cet appel, et nous planifions une méthode de mise à jour dans le Dispatcher du thread UI lorsque nous avons fini.
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class Window1
Inherits Window
' Delegates to be used in placking jobs onto the Dispatcher.
Private Delegate Sub NoArgDelegate()
Private Delegate Sub OneArgDelegate(ByVal arg As String)
' Storyboards for the animations.
Private showClockFaceStoryboard As Storyboard
Private hideClockFaceStoryboard As Storyboard
Private showWeatherImageStoryboard As Storyboard
Private hideWeatherImageStoryboard As Storyboard
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Load the storyboard resources.
showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
End Sub
Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
' Change the status image and start the rotation animation.
fetchButton.IsEnabled = False
fetchButton.Content = "Contacting Server"
weatherText.Text = ""
hideWeatherImageStoryboard.Begin(Me)
' Start fetching the weather forecast asynchronously.
Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
fetcher.BeginInvoke(Nothing, Nothing)
End Sub
Private Sub FetchWeatherFromServer()
' Simulate the delay from network access.
Thread.Sleep(4000)
' Tried and true method for weather forecasting - random numbers.
Dim rand As New Random()
Dim weather As String
If rand.Next(2) = 0 Then
weather = "rainy"
Else
weather = "sunny"
End If
' Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
End Sub
Private Sub UpdateUserInterface(ByVal weather As String)
'Set the weather image
If weather = "sunny" Then
weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
ElseIf weather = "rainy" Then
weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
End If
'Stop clock animation
showClockFaceStoryboard.Stop(Me)
hideClockFaceStoryboard.Begin(Me)
'Update UI text
fetchButton.IsEnabled = True
fetchButton.Content = "Fetch Forecast"
weatherText.Text = weather
End Sub
Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
showWeatherImageStoryboard.Begin(Me)
End Sub
Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
showClockFaceStoryboard.Begin(Me, True)
End Sub
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
// Delegates to be used in placking jobs onto the Dispatcher.
private delegate void NoArgDelegate();
private delegate void OneArgDelegate(String arg);
// Storyboards for the animations.
private Storyboard showClockFaceStoryboard;
private Storyboard hideClockFaceStoryboard;
private Storyboard showWeatherImageStoryboard;
private Storyboard hideWeatherImageStoryboard;
public Window1(): base()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Load the storyboard resources.
showClockFaceStoryboard =
(Storyboard)this.Resources["ShowClockFaceStoryboard"];
hideClockFaceStoryboard =
(Storyboard)this.Resources["HideClockFaceStoryboard"];
showWeatherImageStoryboard =
(Storyboard)this.Resources["ShowWeatherImageStoryboard"];
hideWeatherImageStoryboard =
(Storyboard)this.Resources["HideWeatherImageStoryboard"];
}
private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
hideWeatherImageStoryboard.Begin(this);
// Start fetching the weather forecast asynchronously.
NoArgDelegate fetcher = new NoArgDelegate(
this.FetchWeatherFromServer);
fetcher.BeginInvoke(null, null);
}
private void FetchWeatherFromServer()
{
// Simulate the delay from network access.
Thread.Sleep(4000);
// Tried and true method for weather forecasting - random numbers.
Random rand = new Random();
String weather;
if (rand.Next(2) == 0)
{
weather = "rainy";
}
else
{
weather = "sunny";
}
// Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.Normal,
new OneArgDelegate(UpdateUserInterface),
weather);
}
private void UpdateUserInterface(String weather)
{
//Set the weather image
if (weather == "sunny")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"SunnyImageSource"];
}
else if (weather == "rainy")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"RainingImageSource"];
}
//Stop clock animation
showClockFaceStoryboard.Stop(this);
hideClockFaceStoryboard.Begin(this);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private void HideClockFaceStoryboard_Completed(object sender,
EventArgs args)
{
showWeatherImageStoryboard.Begin(this);
}
private void HideWeatherImageStoryboard_Completed(object sender,
EventArgs args)
{
showClockFaceStoryboard.Begin(this, true);
}
}
}
Les éléments suivants sont quelques-uns des détails à noter.
Création du gestionnaire de bouton
Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" hideWeatherImageStoryboard.Begin(Me) ' Start fetching the weather forecast asynchronously. Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer) fetcher.BeginInvoke(Nothing, Nothing) End Sub
private void ForecastButtonHandler(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; hideWeatherImageStoryboard.Begin(this); // Start fetching the weather forecast asynchronously. NoArgDelegate fetcher = new NoArgDelegate( this.FetchWeatherFromServer); fetcher.BeginInvoke(null, null); }
Lors d'un clic sur le bouton, nous affichons le dessin d'horloge et commençons à l'animer. Nous désactivons le bouton. Nous appelons la méthode FetchWeatherFromServer dans un nouveau thread puis, nous retournons, en permettant au Dispatcher de traiter des événements pendant que nous attendons pour recueillir la prévision météorologique.
Extraction du temps
Private Sub FetchWeatherFromServer() ' Simulate the delay from network access. Thread.Sleep(4000) ' Tried and true method for weather forecasting - random numbers. Dim rand As New Random() Dim weather As String If rand.Next(2) = 0 Then weather = "rainy" Else weather = "sunny" End If ' Schedule the update function in the UI thread. tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather) End Sub
private void FetchWeatherFromServer() { // Simulate the delay from network access. Thread.Sleep(4000); // Tried and true method for weather forecasting - random numbers. Random rand = new Random(); String weather; if (rand.Next(2) == 0) { weather = "rainy"; } else { weather = "sunny"; } // Schedule the update function in the UI thread. tomorrowsWeather.Dispatcher.BeginInvoke( System.Windows.Threading.DispatcherPriority.Normal, new OneArgDelegate(UpdateUserInterface), weather); }
Pour simplifier, nous n'avons pas réellement de code de mise en réseau dans cet exemple. À la place, nous simulons le délai d'accès réseau en mettant en veille notre nouveau thread pendant quatre secondes. Pendant ce temps, le thread d'UI d'origine est toujours en cours d'exécution et répond aux événements. Pour l'illustrer, nous avons laissé une animation en cours d'exécution et les boutons d'agrandissement et de réduction continuent également à fonctionner.
Lorsque le délai est terminé et que nous avons aléatoirement sélectionné nos prévisions météorologiques, il est temps de faire un rapport au thread d'UI. Pour cela, nous planifions un appel à UpdateUserInterface dans le thread d'UI à l'aide du Dispatcher du thread. Nous passons une chaîne décrivant la météo à cet appel de méthode planifié.
Mise à jour de l'UI
Private Sub UpdateUserInterface(ByVal weather As String) 'Set the weather image If weather = "sunny" Then weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource) ElseIf weather = "rainy" Then weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource) End If 'Stop clock animation showClockFaceStoryboard.Stop(Me) hideClockFaceStoryboard.Begin(Me) 'Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weather End Sub
private void UpdateUserInterface(String weather) { //Set the weather image if (weather == "sunny") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "SunnyImageSource"]; } else if (weather == "rainy") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "RainingImageSource"]; } //Stop clock animation showClockFaceStoryboard.Stop(this); hideClockFaceStoryboard.Begin(this); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Lorsque le Dispatcher dans le thread d'UI a le temps, il exécute l'appel planifié à UpdateUserInterface Elle affiche cette image et restaure le bouton « d'extraction de prévisions ».
Fenêtres/threads multiples
Certaines applications WPF requièrent plusieurs fenêtres de niveau supérieur. Il est tout à fait acceptable pour une combinaison Thread/Dispatcher de gérer plusieurs fenêtres, mais plusieurs threads sont parfois préférables. Cela s'avère particulièrement vrai lorsque l'une des fenêtres risque de monopoliser le thread.
L'Explorateur Windows fonctionne de cette manière. Chaque nouvelle fenêtre de l'Explorateur appartient au processus d'origine, mais elle est créée sous le contrôle d'un thread indépendant.
Grâce à un contrôle WPFFrame nous pouvons afficher des pages Web. Nous pouvons facilement créer un simple substitut d'Internet Explorer. Nous commençons avec une fonctionnalité importante, à savoir la possibilité d'ouvrir une nouvelle fenêtre d'explorateur. Lorsque l'utilisateur clique sur le bouton « nouvelle fenêtre », nous lançons une copie de notre fenêtre dans un thread séparé. De cette façon, les opérations à durée d'exécution longue ou bloquantes dans l'une des fenêtres ne verrouilleront pas toutes les autres fenêtres.
En réalité, le modèle de navigateur Web a son propre modèle de thread complexe. Nous l'avons choisi parce qu'il doit être familier à la plupart des lecteurs.
L'exemple suivant montre le code.
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MultiBrowse"
Height="600"
Width="800"
Loaded="OnLoaded"
>
<StackPanel Name="Stack" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button Content="New Window"
Click="NewWindowHandler" />
<TextBox Name="newLocation"
Width="500" />
<Button Content="GO!"
Click="Browse" />
</StackPanel>
<Frame Name="placeHolder"
Width="800"
Height="550"></Frame>
</StackPanel>
</Window>
Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
Partial Public Class Window1
Inherits Window
Public Sub New()
MyBase.New()
InitializeComponent()
End Sub
Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
placeHolder.Source = New Uri("https://www.msn.com")
End Sub
Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
placeHolder.Source = New Uri(newLocation.Text)
End Sub
Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
Private Sub ThreadStartingPoint()
Dim tempWindow As New Window1()
tempWindow.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public Window1() : base()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri("https://www.msn.com");
}
private void Browse(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri(newLocation.Text);
}
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
}
}
Les segments de thread suivants de ce code sont les plus intéressants pour nous dans ce contexte :
Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
Cette méthode est appelée lors d'un clic sur le bouton « nouvelle fenêtre ». Elle crée un nouveau thread et le démarre de façon asynchrone.
Private Sub ThreadStartingPoint()
Dim tempWindow As New Window1()
tempWindow.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
Cette méthode est le point de départ pour le nouveau thread. Nous créons une fenêtre sous le contrôle de ce thread. WPF crée automatiquement un nouveau Dispatcher pour gérer le nouveau thread. Pour rendre la fenêtre fonctionnelle, il nous suffit de lancer le Dispatcher.
Détails techniques et points d'entrave
Écriture de composants à l'aide de threads
Le Guide du développeur Microsoft .NET Framework décrit un modèle expliquant comment un composant peut exposer le comportement asynchrone à ses clients (voir Vue d'ensemble du modèle asynchrone basé sur des événements). Par exemple, supposez que nous souhaitions empaqueter la méthode FetchWeatherFromServer dans un composant réutilisable non graphique. Selon le modèle Microsoft .NET Framework, il présenterait l'aspect suivant :
Public Class WeatherComponent
Inherits Component
'gets weather: Synchronous
Public Function GetWeather() As String
Dim weather As String = ""
'predict the weather
Return weather
End Function
'get weather: Asynchronous
Public Sub GetWeatherAsync()
'get the weather
End Sub
Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
End Class
Public Class GetWeatherCompletedEventArgs
Inherits AsyncCompletedEventArgs
Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
MyBase.New([error], canceled, userState)
_weather = weather
End Sub
Public ReadOnly Property Weather() As String
Get
Return _weather
End Get
End Property
Private _weather As String
End Class
Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
public class WeatherComponent : Component
{
//gets weather: Synchronous
public string GetWeather()
{
string weather = "";
//predict the weather
return weather;
}
//get weather: Asynchronous
public void GetWeatherAsync()
{
//get the weather
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}
public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
public GetWeatherCompletedEventArgs(Exception error, bool canceled,
object userState, string weather)
:
base(error, canceled, userState)
{
_weather = weather;
}
public string Weather
{
get { return _weather; }
}
private string _weather;
}
public delegate void GetWeatherCompletedEventHandler(object sender,
GetWeatherCompletedEventArgs e);
GetWeatherAsync utiliserait l'une des techniques décrites précédemment, telles que créer un thread d'arrière-plan, faire le travail de façon asynchrone, en ne bloquant pas le thread appelant.
L'une des parties les plus importantes de ce modèle appelle la méthode MethodNameCompleted sur le même thread qui a appelé la méthode MethodNameAsync pour commencer. Vous pourriez effectuer assez facilement cette opération à l'aide de WPF en stockant CurrentDispatcher, mais ensuite le composant non graphique pourrait uniquement être utilisé dans les applications WPF et non dans les programmes Windows Forms ou ASP.NET.
La classe DispatcherSynchronizationContext répond à ce besoin. Considérez-la comme une version simplifiée de Dispatcher qui fonctionne aussi avec d'autres structures d'UI.
Public Class WeatherComponent2
Inherits Component
Public Function GetWeather() As String
Return fetchWeatherFromServer()
End Function
Private requestingContext As DispatcherSynchronizationContext = Nothing
Public Sub GetWeatherAsync()
If requestingContext IsNot Nothing Then
Throw New InvalidOperationException("This component can only handle 1 async request at a time")
End If
requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)
Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)
' Launch thread
fetcher.BeginInvoke(Nothing, Nothing)
End Sub
Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
RaiseEvent GetWeatherCompleted(Me, e)
End Sub
Private Function fetchWeatherFromServer() As String
' do stuff
Dim weather As String = ""
Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)
Dim callback As New SendOrPostCallback(AddressOf DoEvent)
requestingContext.Post(callback, e)
requestingContext = Nothing
Return e.Weather
End Function
Private Sub DoEvent(ByVal e As Object)
'do stuff
End Sub
Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
Public Delegate Function NoArgDelegate() As String
End Class
public class WeatherComponent2 : Component
{
public string GetWeather()
{
return fetchWeatherFromServer();
}
private DispatcherSynchronizationContext requestingContext = null;
public void GetWeatherAsync()
{
if (requestingContext != null)
throw new InvalidOperationException("This component can only handle 1 async request at a time");
requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;
NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);
// Launch thread
fetcher.BeginInvoke(null, null);
}
private void RaiseEvent(GetWeatherCompletedEventArgs e)
{
if (GetWeatherCompleted != null)
GetWeatherCompleted(this, e);
}
private string fetchWeatherFromServer()
{
// do stuff
string weather = "";
GetWeatherCompletedEventArgs e =
new GetWeatherCompletedEventArgs(null, false, null, weather);
SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
requestingContext.Post(callback, e);
requestingContext = null;
return e.Weather;
}
private void DoEvent(object e)
{
//do stuff
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
public delegate string NoArgDelegate();
}
Pompage imbriqué
Il est parfois impossible de verrouiller complètement le thread d'UI. Considérons la méthode Show de la classe MessageBox. Show ne retourne pas de valeur tant que l'utilisateur n'a pas cliqué sur le bouton OK. Cela crée toutefois une fenêtre qui doit avoir une boucle de messages pour être interactive. Pendant que nous attendons que l'utilisateur clique sur OK, la fenêtre d'application d'origine ne répond pas à l'entrée de l'utilisateur. Elle continue toutefois à traiter les messages de peinture. La fenêtre d'origine se redessine lorsqu'elle est couverte et révélée.
Un thread doit être responsable de la fenêtre de message. WPF pourrait créer un thread uniquement pour la fenêtre de message, mais ce thread ne pourrait pas peindre les éléments désactivés dans la fenêtre d'origine (souvenez-vous du point précédemment abordé sur l'exclusion mutuelle). Au lieu de cela, WPF utilise un système de traitement de message imbriqué. La classe Dispatcher comprend une méthode spéciale appelée PushFrame, qui stocke le point d'exécution actuel d'une application et commence ensuite une nouvelle boucle de messages. Une fois la boucle de messages imbriquée terminée, l'exécution reprend après l'appel PushFrame d'origine.
Dans ce cas, PushFrame maintient le contexte du programme dans un appel à MessageBox.Show, et il commence une nouvelle boucle de message pour repeindre la fenêtre d'arrière-plan et gérer l'entrée dans la fenêtre de message. Lorsque l'utilisateur clique sur OK et efface la fenêtre indépendante, la boucle imbriquée s'arrête et le contrôle reprend après l'appel à Show.
Événements routés périmés
Le système d'événement routé dans WPF notifie des arborescences entières lorsque les événements sont déclenchés.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
Lorsque vous appuyez avec le bouton gauche de la souris sur l'ellipse, handler2 est exécuté. Après que handler2 a fini, l'événement est passé à l'objet Canvas, qui utilise handler1 pour le traiter. Cela se produit uniquement si handler2 ne marque pas l'objet d'événement explicitement comme géré.
Il est possible que handler2 mette beaucoup de temps à traiter cet événement. handler2 peut utiliser PushFrame pour commencer une boucle de message imbriquée qui n'est pas retournée avant des heures. Si handler2 ne marque pas l'événement comme géré lorsque cette boucle de message est terminée, l'événement est passé en haut de l'arborescence même s'il est très ancien.
Réentrance et verrouillage
Le mécanisme de verrouillage du common language runtime (CLR) ne se comporte pas exactement comme nous pouvons l'imaginer ; nous pouvons nous attendre à ce qu'un thread cesse complètement l'opération lors d'une demande de verrou. En réalité, le thread continue à recevoir et à traiter les messages prioritaires. Cela permet d'empêcher des interblocages et de rendre des interfaces moins réactives, mais introduit la possibilité de bogues subtils. La plupart du temps, vous n'avez pas besoin de tout savoir sur ce sujet, mais dans de rares circonstances (impliquant généralement des messages de fenêtre Win32 ou des composants COM STA), cela peut s'avérer utile.
La plupart des interfaces ne sont pas construites avec la sécurité des threads en tête, car les développeurs partent du principe que plusieurs threads n'accèdent jamais à une UI. Dans ce cas, ce thread unique peut apporter des modifications environnementales à des moments inattendus, en provoquant des effets négatifs que le mécanisme d'exclusion mutuelle DispatcherObject est supposé résoudre. Considérez le pseudo-code suivant :
La plupart du temps cela convient, mais ce type de réentrance inattendue risque parfois de créer de véritables problèmes dans WPF. C'est la raison pour laquelle, à certains moments importants, WPF appelle DisableProcessing, qui modifie l'instruction de verrou pour que ce thread utilise le verrou sans réentrance WPF au lieu du verrou CLR habituel.
Pourquoi l'équipe CLR a-t-elle donc choisi ce comportement ? Cela est lié aux objets COM STA et au thread de finalisation. Lorsqu'un objet est récupéré par le garbage collector, sa méthode Finalize est exécutée sur le thread finaliseur dédié, et non le thread d'UI. Et c'est bien là l'origine du problème, car un objet COM STA qui a été créé sur le thread d'UI ne peut être supprimé que sur le thread d'UI. Le CLR fait l'équivalent d'un BeginInvoke (dans ce cas à l'aide du SendMessage de Win32). Mais si le thread d'UI est occupé, le thread finaliseur est bloqué et l'objet COM STA ne peut pas être supprimé, ce qui engendre une grave fuite de mémoire. L'équipe CLR a donc pris la décision difficile de faire fonctionner les verrous de cette manière.
La tâche pour WPF est éviter la réentrance inattendue sans réintroduire la fuite de mémoire, c'est pourquoi nous ne bloquons pas la réentrance partout.