Modèle de thread

Windows Presentation Foundation (WPF) est conçu pour sauver les développeurs des difficultés de thread. Par conséquent, la plupart des développeurs WPF n’écrivent pas d’interface qui utilise plusieurs threads. Comme les programmes multithreads sont complexes et difficiles à déboguer, il est préférable de les éviter quand des solutions à thread unique existent.

Quel que soit le mode d’architecture, aucune infrastructure d’interface utilisateur n’est en mesure de fournir une solution à thread unique pour chaque type de problème. WPF se rapproche, mais il existe toujours des situations où plusieurs threads améliorent la réactivité de l’interface utilisateur ou les performances de l’application. Après avoir discuté de certains documents d’arrière-plan, cet article explore certaines de ces situations, puis se termine par une discussion sur certains détails de niveau inférieur.

Remarque

Cette rubrique traite du threading à l’aide de la InvokeAsync méthode pour les appels asynchrones. La InvokeAsync méthode prend un Action ou Func<TResult> en tant que paramètre et retourne un DispatcherOperation ou DispatcherOperation<TResult>, qui a une Task propriété. Vous pouvez utiliser l’mot clé await avec le DispatcherOperation ou l’associé Task. Si vous avez besoin d’attendre de façon synchrone pour l’élément Task retourné par un DispatcherOperation ou DispatcherOperation<TResult>, appelez la DispatcherOperationWait méthode d’extension. L’appel Task.Wait entraîne un blocage. Pour plus d’informations sur l’utilisation d’une Task opération asynchrone, consultez programmation asynchrone basée sur les tâches.

Pour effectuer un appel synchrone, utilisez la Invoke méthode, qui a également des surcharges qui acceptent un délégué, Actionou Func<TResult> un paramètre.

Vue d’ensemble et répartiteur

En règle générale, les applications WPF commencent par deux threads : une pour gérer le rendu et une autre pour la gestion de l’interface utilisateur. Le thread de rendu s’exécute efficacement masqué en arrière-plan tandis que le thread d’interface utilisateur reçoit des entrées, gère les événements, peint l’écran et exécute du code d’application. La plupart des applications utilisent un thread d’interface utilisateur unique, même si dans certaines situations, il est préférable d’utiliser plusieurs. Nous aborderons cela avec un exemple plus tard.

Le thread d’interface utilisateur met en file d’attente les éléments de travail à l’intérieur d’un objet appelé .Dispatcher Le Dispatcher sélectionne des éléments de travail en fonction de l'ordre de priorité et exécute chacun d'eux jusqu'à leur achèvement. Chaque thread d’interface utilisateur doit avoir au moins un Dispatcherthread, et chacun Dispatcher peut exécuter des éléments de travail dans exactement un thread.

L’astuce pour créer des applications réactives et conviviales consiste à maximiser le Dispatcher débit en conservant les éléments de travail petits. De cette façon, les éléments ne sont jamais obsolètes dans la Dispatcher file d’attente en attente de traitement. Tout délai perceptible entre une entrée et sa réponse peut frustrer un utilisateur.

Comment les applications WPF sont-elles censées gérer les grandes opérations ? Que se passe-t-il si votre code implique un grand calcul ou doit interroger une base de données sur un serveur distant ? En règle générale, la réponse consiste à gérer l’opération volumineuse dans un thread distinct, laissant le thread d’interface utilisateur libre pour avoir tendance à avoir des éléments dans la Dispatcher file d’attente. Une fois l’opération volumineuse terminée, elle peut renvoyer son résultat au thread d’interface utilisateur pour l’affichage.

Historiquement, Windows autorise l’accès aux éléments d’interface utilisateur uniquement par le thread qui les a créés. Cela signifie qu’un thread d’arrière-plan chargé des tâches d’exécution longue ne peut pas mettre à jour une zone de texte quand il est terminé. Windows effectue cette opération pour garantir l’intégrité des composants de l’interface utilisateur. Une zone de liste pourrait sembler étrange si son contenu était mis à jour par un thread d’arrière-plan pendant la phase de dessin.

WPF dispose d’un mécanisme d’exclusion mutuelle intégré qui applique cette coordination. La plupart des classes dans WPF dérivent de DispatcherObject. Lors de la construction, un DispatcherObject magasine une référence au Dispatcher thread en cours d’exécution. En effet, l’associé DispatcherObject au thread qui le crée. Pendant l’exécution du programme, un DispatcherObject peut appeler sa méthode publique VerifyAccess . VerifyAccess examine l’associé Dispatcher au thread actuel et le compare à la référence stockée pendant la Dispatcher construction. S’ils ne correspondent pas, VerifyAccess lève une exception. VerifyAccess est destiné à être appelé au début de chaque méthode appartenant à un DispatcherObject.

Si un seul thread peut modifier l’interface utilisateur, comment les threads d’arrière-plan interagissent-ils avec l’utilisateur ? Un thread d’arrière-plan peut demander au thread d’interface utilisateur d’effectuer une opération en son nom. Pour ce faire, inscrivez un élément de travail auprès Dispatcher du thread d’interface utilisateur. La Dispatcher classe fournit les méthodes permettant d’inscrire des éléments de travail : Dispatcher.InvokeAsync, Dispatcher.BeginInvokeet Dispatcher.Invoke. Ces méthodes planifient un délégué pour l’exécution. Invoke est un appel synchrone , autrement dit, il ne retourne pas tant que le thread d’interface utilisateur n’a pas terminé l’exécution du délégué. InvokeAsync et BeginInvoke sont asynchrones et renvoyées immédiatement.

Commande Dispatcher les éléments dans sa file d’attente par priorité. Dix niveaux peuvent être spécifiés lors de l’ajout d’un élément à la Dispatcher file d’attente. Ces priorités sont conservées dans l’énumération DispatcherPriority .

Application monothread avec un calcul de longue durée

La plupart des interfaces utilisateur graphiques passent une grande partie de leur temps inactif pendant l’attente d’événements générés en réponse aux interactions utilisateur. Avec une programmation minutieuse, cette durée d’inactivité peut être utilisée de manière constructive, sans affecter la réactivité de l’interface utilisateur. Le modèle de thread WPF n’autorise pas l’entrée à interrompre une opération dans le thread d’interface utilisateur. Cela signifie que vous devez être sûr de revenir Dispatcher régulièrement pour traiter les événements d’entrée en attente avant qu’ils ne soient obsolètes.

Un exemple d’application illustrant les concepts de cette section peut être téléchargé à partir de GitHub pour C# ou Visual Basic.

Prenons l’exemple suivant :

Screenshot that shows threading of prime numbers.

Cette application simple compte à partir de trois de façon croissante, en recherchant les nombres premiers. Quand l’utilisateur clique sur le bouton Start, la recherche commence. Quand le programme trouve un nombre premier, il met à jour l’interface utilisateur avec sa découverte. À tout moment, l’utilisateur peut arrêter la recherche.

Bien qu’assez simple, la recherche des nombres premiers peut être illimitée dans le temps, ce qui présente quelques difficultés. Si nous avons géré toute la recherche à l’intérieur du gestionnaire d’événements click du bouton, nous ne donnerions jamais au thread d’interface utilisateur la possibilité de gérer d’autres événements. L’interface utilisateur ne peut pas répondre aux messages d’entrée ou de traitement. Elle ne redessinerait jamais l’interface et ne répondrait jamais aux clics sur le bouton.

Nous aurions pu effectuer la recherche des nombres premiers dans un thread distinct, mais nous devrions alors faire face à des problèmes de synchronisation. Avec une approche à thread unique, nous pouvons directement mettre à jour le libellé qui indique le plus grand nombre premier trouvé.

Si nous divisez la tâche de calcul en blocs gérables, nous pouvons revenir régulièrement aux Dispatcher événements et traiter les événements. Nous pouvons donner à WPF l’occasion de repeindre et de traiter les entrées.

La meilleure façon de fractionner le temps de traitement entre le calcul et la gestion des événements consiste à gérer le calcul à partir du Dispatcher. À l’aide de la InvokeAsync méthode, nous pouvons planifier des case activée de nombre premier dans la même file d’attente à partir de laquelle les événements d’interface utilisateur sont tirés. Dans notre exemple, nous planifions une seule vérification de nombre premier à la fois. Une fois la vérification de nombre premier terminée, nous planifions immédiatement la vérification suivante. Cette case activée se poursuit uniquement après que les événements d’interface utilisateur en attente ont été gérés.

Screenshot that shows the dispatcher queue.

Microsoft Word effectue des case activée orthographiques à l’aide de ce mécanisme. Le case activée orthographique est effectué en arrière-plan à l’aide de l’heure d’inactivité du thread d’interface utilisateur. Regardons ce code.

L’exemple suivant montre le code XAML qui crée l’interface utilisateur.

Important

Le code XAML présenté dans cet article provient d’un projet C#. Visual Basic XAML est légèrement différent lors de la déclaration de la classe de stockage pour le code XAML.

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>

L’exemple suivant montre le code-behind.

using System;
using System.Windows;
using System.Windows.Threading;

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

            for (long i = 3; i <= Math.Sqrt(_num); i++)
            {
                if (_num % i == 0)
                {
                    // Set not a prime flag to true.
                    _isPrime = false;
                    break;
                }
            }

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}
Imports System.Windows.Threading

Public Class PrimeNumber
    ' Current number to check
    Private _num As Long = 3
    Private _runCalculation As Boolean = False

    Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
        _runCalculation = Not _runCalculation

        If _runCalculation Then
            StartStopButton.Content = "Stop"
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        Else
            StartStopButton.Content = "Resume"
        End If

    End Sub

    Public Sub CheckNextNumber()
        ' Reset flag.
        _isPrime = True

        For i As Long = 3 To Math.Sqrt(_num)
            If (_num Mod i = 0) Then

                ' Set Not a prime flag to true.
                _isPrime = False
                Exit For
            End If
        Next

        ' If a prime number, update the UI text
        If _isPrime Then
            bigPrime.Text = _num.ToString()
        End If

        _num += 2

        ' Requeue this method on the dispatcher
        If (_runCalculation) Then
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        End If
    End Sub

    Private _isPrime As Boolean
End Class

Outre la mise à jour du texte sur le Buttongestionnaire, le StartStopButton_Click gestionnaire est chargé de planifier le premier nombre premier case activée en ajoutant un délégué à la Dispatcher file d’attente. Une fois que ce gestionnaire d’événements a terminé son travail, le Dispatcher délégué est sélectionné pour l’exécution.

Comme nous l’avons mentionné précédemment, InvokeAsync est le Dispatcher membre utilisé pour planifier un délégué pour l’exécution. Dans ce cas, nous choisissons la SystemIdle priorité. Le Dispatcher délégué n’exécute ce délégué qu’en l’absence d’événements importants à traiter. La réactivité de l’interface utilisateur est plus importante que le nombre case activée ing. Nous passons également un nouveau délégué qui représente la routine de vérification des nombres.

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

    for (long i = 3; i <= Math.Sqrt(_num); i++)
    {
        if (_num % i == 0)
        {
            // Set not a prime flag to true.
            _isPrime = false;
            break;
        }
    }

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    _isPrime = True

    For i As Long = 3 To Math.Sqrt(_num)
        If (_num Mod i = 0) Then

            ' Set Not a prime flag to true.
            _isPrime = False
            Exit For
        End If
    Next

    ' If a prime number, update the UI text
    If _isPrime Then
        bigPrime.Text = _num.ToString()
    End If

    _num += 2

    ' Requeue this method on the dispatcher
    If (_runCalculation) Then
        StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
    End If
End Sub

Private _isPrime As Boolean

Cette méthode vérifie si le nombre impair suivant est un nombre premier. S’il est de premier choix, la méthode met directement à jour la bigPrimeTextBlock méthode pour refléter sa découverte. Nous pouvons le faire, car le calcul se produit dans le même thread utilisé pour créer le contrôle. Si nous avons choisi d’utiliser un thread distinct pour le calcul, nous devons utiliser un mécanisme de synchronisation plus complexe et exécuter la mise à jour dans le thread d’interface utilisateur. Nous allons démontrer cette situation ensuite.

Plusieurs fenêtres, plusieurs threads

Certaines applications WPF nécessitent plusieurs fenêtres de niveau supérieur. Il est parfaitement acceptable pour une combinaison Thread/Dispatcher de gérer plusieurs fenêtres, mais parfois plusieurs threads font un meilleur travail. Cela est particulièrement vrai s’il y a une chance que l’une des fenêtres monopolise le thread.

L’Explorateur Windows fonctionne de cette manière. Chaque nouvelle fenêtre Explorateur appartient au processus d’origine, mais elle est créée sous le contrôle d’un thread indépendant. Lorsque l’Explorateur ne répond pas, par exemple lorsque vous recherchez des ressources réseau, d’autres fenêtres de l’Explorateur continuent d’être réactives et utilisables.

Nous pouvons illustrer ce concept avec l’exemple suivant.

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

Les trois premières fenêtres de cette image partagent le même identificateur de thread : 1. Les deux autres fenêtres ont des identificateurs de thread différents : Neuf et 4. Il y a une rotation en magenta couleur ! !️ glyphe en haut à droite de chaque fenêtre.

Cet exemple contient une fenêtre avec un glyphe pivotant ‼️ , un bouton Pause et deux autres boutons qui créent une fenêtre sous le thread actuel ou dans un nouveau thread. Le ‼️ glyphe pivote constamment jusqu’à ce que le bouton Pause soit enfoncé, ce qui interrompt le thread pendant cinq secondes. En bas de la fenêtre, l’identificateur de thread s’affiche.

Lorsque le bouton Pause est enfoncé, toutes les fenêtres situées sous le même thread ne répondent pas. Toute fenêtre sous un autre thread continue de fonctionner normalement.

L’exemple suivant est le code XAML de la fenêtre :

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>

L’exemple suivant montre le code-behind.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

        private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
        {
            Thread newWindowThread = new Thread(ThreadStartingPoint);
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start();
        }

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}
Imports System.Threading

Public Class MultiWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
    End Sub

    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub

    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub

    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub

    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()

        System.Windows.Threading.Dispatcher.Run()
    End Sub
End Class

Voici quelques-uns des détails à noter :

  • La Task.Delay(TimeSpan) tâche est utilisée pour mettre le thread actuel en pause pendant cinq secondes lorsque le bouton Suspendre est enfoncé.

    private void PauseButton_Click(object sender, RoutedEventArgs e) =>
        Task.Delay(TimeSpan.FromSeconds(5)).Wait();
    
    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub
    
  • Le SameThreadWindow_Click gestionnaire d’événements affiche de façon immédante une nouvelle fenêtre sous le thread actuel. Le NewThreadWindow_Click gestionnaire d’événements crée un thread qui commence à exécuter la ThreadStartingPoint méthode, qui affiche à son tour une nouvelle fenêtre, comme décrit dans le point de puce suivant.

    private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
        new MultiWindow().Show();
    
    private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
    {
        Thread newWindowThread = new Thread(ThreadStartingPoint);
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    }
    
    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub
    
    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    
  • La ThreadStartingPoint méthode est le point de départ du nouveau thread. La nouvelle fenêtre est créée sous le contrôle de ce thread. WPF crée automatiquement un System.Windows.Threading.Dispatcher nouveau thread pour gérer le nouveau thread. Tout ce que nous devons faire pour que la fenêtre fonctionne est de démarrer le System.Windows.Threading.Dispatcher.

    private void ThreadStartingPoint()
    {
        new MultiWindow().Show();
    
        System.Windows.Threading.Dispatcher.Run();
    }
    
    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()
    
        System.Windows.Threading.Dispatcher.Run()
    End Sub
    

Un exemple d’application illustrant les concepts de cette section peut être téléchargé à partir de GitHub pour C# ou Visual Basic.

Gérer une opération bloquante avec Task.Run

La gestion des opérations bloquantes dans une application graphique peuvent être difficile. Nous ne voulons pas appeler des méthodes bloquantes à partir de gestionnaires d’événements, car l’application semble figer. L’exemple précédent a créé de nouvelles fenêtres dans leur propre thread, ce qui permet à chaque fenêtre de s’exécuter indépendamment les unes des autres. Bien que nous puissions créer un thread avec System.Windows.Threading.Dispatcher, il devient difficile de synchroniser le nouveau thread avec le thread d’interface utilisateur principal une fois le travail terminé. Étant donné que le nouveau thread ne peut pas modifier directement l’interface utilisateur, nous devons utiliser Dispatcher.InvokeAsync, Dispatcher.BeginInvokeou , pour Dispatcher.Invokeinsérer des délégués dans le Dispatcher thread d’interface utilisateur. Finalement, ces délégués sont exécutés avec l’autorisation de modifier des éléments d’interface utilisateur.

Il existe un moyen plus simple d’exécuter le code sur un nouveau thread tout en synchronisant les résultats, le modèle asynchrone basé sur les tâches (TAP) . Elle est basée sur les types et Task<TResult> les Task types dans l’espace System.Threading.Tasks de noms, qui sont utilisés pour représenter des opérations asynchrones. Le TAP utilise une méthode unique pour représenter le début et la fin d'une opération asynchrone. Il existe quelques avantages pour ce modèle :

  • L’appelant d’un Task peut choisir d’exécuter le code de manière asynchrone ou synchrone.
  • La progression peut être signalée à partir du Task.
  • Le code appelant peut interrompre l’exécution et attendre le résultat de l’opération.

Exemple Task.Run

Dans cet exemple, nous simulons un appel de procédure distante qui récupère une prévision météorologique. Lorsque le bouton est cliqué, l’interface utilisateur est mise à jour pour indiquer que la récupération des données est en cours, tandis qu’une tâche est démarrée pour imiter l’extraction des prévisions météorologiques. Une fois la tâche démarrée, le code du gestionnaire d’événements de bouton est suspendu jusqu’à ce que la tâche se termine. Une fois la tâche terminée, le code du gestionnaire d’événements continue à s’exécuter. Le code est suspendu et ne bloque pas le reste du thread d’interface utilisateur. Le contexte de synchronisation de WPF gère la suspension du code, ce qui permet à WPF de continuer à s’exécuter.

A diagram that demonstrates the workflow of the example app.

Diagramme illustrant le flux de travail de l’exemple d’application. L’application a un bouton unique avec le texte « Fetch Forecast ». Une flèche pointe vers la phase suivante de l’application une fois le bouton enfoncé, qui est une image d’horloge placée au centre de l’application indiquant que l’application est occupée à extraire des données. Après un certain temps, l’application retourne une image du soleil ou des nuages de pluie, en fonction du résultat des données.

Un exemple d’application illustrant les concepts de cette section peut être téléchargé à partir de GitHub pour C# ou Visual Basic. Le code XAML de cet exemple est assez volumineux et n’est pas fourni dans cet article. Utilisez les liens GitHub précédents pour parcourir le code XAML. Le code XAML utilise un bouton unique pour récupérer le temps.

Considérez le code-behind dans le code XAML :

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

            // Tried and true method for weather forecasting - random numbers
            Random rand = new Random();

            if (rand.Next(2) == 0)
                return "rainy";
            
            else
                return "sunny";
        }

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}
Imports System.Windows.Media.Animation

Public Class Weather

    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)

        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)

        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)

        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)

        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)

        End If

        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)

        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub

    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)

        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))

        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()

        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If

    End Function

    Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
    End Sub

    Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
    End Sub
End Class

Voici quelques-uns des détails à noter.

  • Gestionnaire d’événements de bouton

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Notez que le gestionnaire d’événements a été déclaré avec async (ou Async avec Visual Basic). Une méthode « async » autorise la suspension du code lorsqu’une méthode attendue, telle que FetchWeatherFromServerAsync, est appelée. Cela est désigné par l’mot clé await (ou Await avec Visual Basic). Jusqu’à la FetchWeatherFromServerAsync fin, le code du gestionnaire du bouton est suspendu et le contrôle est retourné à l’appelant. Cela est similaire à une méthode synchrone, sauf qu’une méthode synchrone attend que chaque opération de la méthode se termine après quoi le contrôle est retourné à l’appelant.

    Les méthodes attendues utilisent le contexte de thread de la méthode actuelle, qui avec le gestionnaire de boutons, est le thread d’interface utilisateur. Cela signifie que l’appel await FetchWeatherFromServerAsync(); (ou Await FetchWeatherFromServerAsync() avec Visual Basic) entraîne l’exécution du code FetchWeatherFromServerAsync sur le thread d’interface utilisateur, mais n’est pas exécuté sur le répartiteur a le temps de l’exécuter, comme l’application monothread avec un exemple de calcul long fonctionne. Toutefois, notez qu’il await Task.Run est utilisé. Cela crée un thread sur le pool de threads pour la tâche désignée au lieu du thread actuel. Il FetchWeatherFromServerAsync s’exécute donc sur son propre thread.

  • Récupération de la météo

    private async Task<string> FetchWeatherFromServerAsync()
    {
        // Simulate the delay from network access
        await Task.Delay(TimeSpan.FromSeconds(4));
    
        // Tried and true method for weather forecasting - random numbers
        Random rand = new Random();
    
        if (rand.Next(2) == 0)
            return "rainy";
        
        else
            return "sunny";
    }
    
    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
    
        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))
    
        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()
    
        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If
    
    End Function
    

    Pour simplifier les choses, nous n’avons pas de code réseau dans cet exemple. Au lieu de cela, nous simulons le délai d’accès réseau en plaçant notre nouveau thread en veille pendant quatre secondes. Dans ce temps, le thread d’interface utilisateur d’origine est toujours en cours d’exécution et répond aux événements d’interface utilisateur pendant que le gestionnaire d’événements du bouton est suspendu jusqu’à ce que le nouveau thread se termine. Pour illustrer cela, nous avons laissé une animation en cours d’exécution et vous pouvez redimensionner la fenêtre. Si le thread d’interface utilisateur a été suspendu ou retardé, l’animation ne s’affiche pas et vous n’avez pas pu interagir avec la fenêtre.

    Une fois l’opération Task.Delay terminée, et nous avons sélectionné de façon aléatoire nos prévisions météorologiques, l’état météorologique est retourné à l’appelant.

  • Mise à jour de l’interface utilisateur

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Une fois la tâche terminée et que le thread d’interface utilisateur a le temps, l’appelant du Task.Rungestionnaire d’événements du bouton est repris. Le reste de la méthode arrête l’animation de l’horloge et choisit une image pour décrire le temps. Il affiche cette image et active le bouton « extraire la prévision ».

Un exemple d’application illustrant les concepts de cette section peut être téléchargé à partir de GitHub pour C# ou Visual Basic.

Détails techniques et points d’achoppement

Les sections suivantes décrivent certains des détails et des points d’achoppement que vous pouvez rencontrer avec le multithreading.

Pompe imbriquée

Parfois, il n’est pas possible de verrouiller complètement le thread d’interface utilisateur. Prenons la Show méthode de la MessageBox classe. Show ne retourne pas tant que l’utilisateur n’a pas cliqué sur le bouton OK. Il peut cependant créer 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 aux entrées utilisateur. Elle continue cependant de traiter les messages concernant le dessin. La fenêtre d’origine se redessine elle-même quand elle est couverte et découverte.

Screenshot that shows a MessageBox with an OK button

Un thread doit être chargé de la fenêtre de la boîte de message. WPF peut créer un nouveau thread uniquement pour la fenêtre de boîte de message, mais ce thread ne peut pas peindre les éléments désactivés dans la fenêtre d’origine (n’oubliez pas la discussion précédente de l’exclusion mutuelle). Au lieu de cela, WPF utilise un système de traitement de messages imbriqué. La Dispatcher classe inclut une méthode spéciale appelée PushFrame, qui stocke le point d’exécution actuel d’une application, puis commence une nouvelle boucle de message. Une fois la boucle de message imbriquée terminée, l’exécution reprend après l’appel d’origine PushFrame .

Dans ce cas, PushFrame conserve le contexte du programme à l’appel MessageBox.Showet démarre 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 boîte de message. Lorsque l’utilisateur clique sur OK et efface la fenêtre contextuelle, la boucle imbriquée se ferme et le contrôle reprend après l’appel.Show

Événements routés obsolètes

Le système d’événements routé dans WPF avertit les arborescences entières lorsque des é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 le bouton gauche de la souris est enfoncé sur l’ellipse, handler2 il est exécuté. Une fois handler2 terminé, l’événement est transmis à l’objet Canvas , qui l’utilise handler1 pour le traiter. Cela se produit uniquement s’il handler2 ne marque pas explicitement l’objet d’événement comme géré.

Il est possible que handler2 cela prenne beaucoup de temps pour traiter cet événement. handler2 peut utiliser PushFrame pour commencer une boucle de message imbriquée qui ne retourne pas pendant des heures. S’il handler2 ne marque pas l’événement comme géré lorsque cette boucle de message est terminée, l’événement est transmis à l’arborescence même si elle est très ancienne.

Réentrance et verrouillage

Le mécanisme de verrouillage du Common Language Runtime (CLR) ne se comporte pas exactement comme on peut l’imaginer ; on peut s’attendre à ce qu’un thread cesse complètement d’fonctionner lors de la demande d’un verrou. En fait, le thread continue à recevoir et à traiter les messages de priorité élevée. Ceci permet d’éviter les blocages et de rendre les interfaces un minimum réactives, mais introduit la possibilité de bogues subtils. La grande majorité du temps que vous n’avez pas besoin de connaître cela, mais dans de rares circonstances (généralement impliquant des messages de fenêtre Win32 ou des composants COM STA) cela peut être utile de connaître.

La plupart des interfaces ne sont pas générées à l’esprit avec la sécurité des threads, car les développeurs travaillent selon l’hypothèse qu’une interface utilisateur n’est jamais accessible par plusieurs threads. Dans ce cas, ce thread unique peut apporter des changements environnementaux à des moments inattendus, provoquant ces effets malades que le DispatcherObject mécanisme d’exclusion mutuelle est censé résoudre. Considérez le pseudocode suivant :

Diagram that shows threading reentrancy.

La plupart du temps, c’est la bonne chose, mais il y a des moments dans WPF où une telle réentrance inattendue peut vraiment causer des problèmes. Par conséquent, à certains moments clés, WPF appelle DisableProcessing, qui modifie l’instruction de verrouillage pour ce thread afin d’utiliser le verrou sans reentrancy-free WPF, au lieu du verrou CLR habituel.

Pourquoi l’équipe CLR a-t-elle choisi ce comportement ? Elle devait tenir compte des objets STA COM et du thread de finalisation. Lorsqu’un objet est collecté par la mémoire, sa Finalize méthode est exécutée sur le thread de finaliseur dédié, et non sur le thread d’interface utilisateur. Et il y a le problème, car un objet COM STA créé sur le thread d’interface utilisateur ne peut être supprimé que sur le thread d’interface utilisateur. Le CLR effectue l’équivalent d’un BeginInvoke (dans ce cas à l’aide de SendMessageWin32). Mais si le thread d’interface utilisateur est occupé, le thread finaliseur est bloqué et l’objet COM STA ne peut pas être supprimé, ce qui crée une fuite de mémoire grave. L’équipe clR a donc fait l’appel difficile pour faire fonctionner les verrous comme ils le font.

La tâche pour WPF consiste à éviter une réentrance inattendue sans introduire à nouveau la fuite de mémoire, c’est pourquoi nous ne bloquez pas la réentrance partout.

Voir aussi