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 :

Capture d'écran : nombres premiers

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.

Illustration de la file d'attente de Dispatcher

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.

Capture d'écran : interface utilisateur météo


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 MessageBoxShow 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. 

MessageBox avec bouton "OK"

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 :

Diagramme de réentrance des threads

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.

Voir aussi

Autres ressources

Exemple d'application monothread impliquant un calcul de longue durée (page éventuellement en anglais)