Modèle de thread

Mise à jour : novembre 2007

Windows Presentation Foundation (WPF) est 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'interface utilisateur 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'interface utilisateur (UI) ou les performances de l'application. Ce document donne quelques informations d'ordre général, puis étudie certaines de ces situations et se termine en abordant des points précis.

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'interface utilisateur. Le thread de rendu s'exécute efficacement masqué en arrière-plan pendant que le thread d'interface utilisateur 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'interface utilisateur, 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'interface utilisateur 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'interface utilisateur 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'interface utilisateur 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'interface utilisateur et l'afficher.

Historiquement, Windows permet d'accéder à des éléments d'interface utilisateur 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'interface utilisateur. 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 DispatcherObject peut appeler sa méthode VerifyAccess publique. VerifyAccess examine le Dispatcher associé au thread actif 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'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, il enregistre un élément de travail avec le Dispatcher du thread d'interface utilisateur. 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'interface utilisateur 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 Kit de développement logiciel (SDK) Windows.

Threads en action : exemples

Application monothread avec calcul de longue durée

La majorité des interfaces graphiques utilisateur (GUI, Graphical User Interfaces) 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'interface utilisateur. Le modèle de thread WPF ne permet pas à l'entrée d'interrompre une opération qui se produit dans le thread d'interface utilisateur. 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'interface utilisateur de gérer d'autres événements. L'interface utilisateur 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'interface utilisateur 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'interface utilisateur 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'interface utilisateur. 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>

Le code suivant montre le code-behind.

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 ture.
                    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 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 en l'absence d'événements importants à traiter. La réactivité de l'interface utilisateur 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 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 ture.
            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'interface utilisateur. Nous illustrerons cette situation par la suite.

Pour obtenir le code source complet pour cet exemple, consultez Application monothread avec calcul de longue durée, exemple.

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'interface utilisateur, car nous ne pouvons pas modifier directement l'interface GUI de notre thread de travail. Nous pouvons utiliser Invoke ou BeginInvoke pour insérer des délégués dans le Dispatcher du thread d'interface utilisateur. Finalement, ces délégués seront exécutés avec l'autorisation de modifier des éléments de l'interface utilisateur.

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 interface utilisateur lorsque nous avons fini.

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

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 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 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'interface utilisateur 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.

  • Mise à jour de l'interface utilisateur

    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'interface utilisateur a le temps, il exécute l'appel planifié pour UpdateUserInterface. Cette méthode arrête l'animation d'horloge et choisit une image pour décrire le temps. Elle affiche cette image et restaure le bouton « d'extraction de prévisions ».

Pour obtenir le code source complet pour cet exemple, consultez Simulation du service Weather via Dispatcher, exemple.

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 Frame WPF, 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>
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 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 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 Dispatcher pour gérer le nouveau thread. Pour rendre la fenêtre fonctionnelle, il nous suffit de lancer le Dispatcher.

Pour obtenir le code source complet pour cet exemple, consultez Navigateur Web multithreading, exemple.

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 souhaitons empaqueter la méthode FetchWeatherFromServer dans un composant réutilisable non graphique. Selon le modèle Microsoft .NET Framework standard, il présenterait l'aspect suivant :

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'interface utilisateur.

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'interface utilisateur. Considérons la méthode Show de la classe MessageBox. Show ne retourne pas de valeur tant que l'utilisateur n'a pas cliqué sur le bouton OK. Cela crée toutefois une fenêtre qui doit avoir une boucle de messages pour être interactive. Pendant que nous attendons que l'utilisateur clique sur OK, la fenêtre d'application d'origine ne répond pas à l'entrée de l'utilisateur. Elle continue toutefois à traiter les messages de peinture. La fenêtre d'origine se redessine lorsqu'elle est couverte et révélée. 

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 inclut 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 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. 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 blocages 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 interface utilisateur. 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'interface utilisateur. Et c'est bien là l'origine du problème, car un objet COM STA qui a été créé sur le thread d'interface utilisateur ne peut être supprimé que sur le thread d'interface utilisateur. Le CLR fait l'équivalent d'un BeginInvoke (dans ce cas à l'aide du SendMessage de Win32). 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 engendre une grave fuite de mémoire. L'équipe CLR a donc généré cet appel complexe pour que les verrous fonctionnent 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

Tâches

Application monothread avec calcul de longue durée, exemple

Simulation du service Weather via Dispatcher, exemple

Navigateur Web multithreading, exemple