Modello di threading

Windows Presentation Foundation (WPF) è progettato per salvare gli sviluppatori dalle difficoltà del threading. Di conseguenza, la maggior parte degli sviluppatori WPF non scrive un'interfaccia che usa più di un thread. Poiché i programmi con multithreading sono complessi ed è difficile eseguirne il debug, è preferibile evitarli quando sono disponibili soluzioni a thread singolo.

Non importa quanto sia ben progettato, tuttavia, nessun framework dell'interfaccia utente è in grado di fornire una soluzione a thread singolo per ogni tipo di problema. WPF viene chiuso, ma esistono ancora situazioni in cui più thread migliorano la velocità di risposta dell'interfaccia utente o le prestazioni dell'applicazione. Dopo aver discusso alcuni materiali di base, questo articolo esplora alcune di queste situazioni e quindi conclude con una discussione di alcuni dettagli di livello inferiore.

Nota

In questo argomento viene illustrato il threading usando il InvokeAsync metodo per le chiamate asincrone. Il InvokeAsync metodo accetta un oggetto Action o Func<TResult> come parametro e restituisce un DispatcherOperation oggetto o DispatcherOperation<TResult>, che ha una Task proprietà . È possibile usare la await parola chiave con o con l'oggetto DispatcherOperation associato Task. Se è necessario attendere in modo sincrono per l'oggetto Task restituito da o DispatcherOperationDispatcherOperation<TResult>, chiamare il DispatcherOperationWait metodo di estensione. La chiamata Task.Wait comporterà un deadlock. Per altre informazioni sull'uso di per Task eseguire operazioni asincrone, vedere Programmazione asincrona basata su attività.

Per effettuare una chiamata sincrona, usare il Invoke metodo , che include anche overload che accettano un delegato, Action, o Func<TResult> parametro .

Panoramica e dispatcher

In genere, le applicazioni WPF iniziano con due thread: uno per la gestione del rendering e un altro per la gestione dell'interfaccia utente. Il thread di rendering viene eseguito in modo efficace nascosto in background mentre il thread dell'interfaccia utente riceve input, gestisce gli eventi, disegna lo schermo ed esegue il codice dell'applicazione. La maggior parte delle applicazioni usa un singolo thread dell'interfaccia utente, anche se in alcune situazioni è preferibile usare diversi. Questo argomento verrà illustrato più avanti con un esempio.

Il thread dell'interfaccia utente accoda gli elementi di lavoro all'interno di un oggetto denominato Dispatcher. L'oggetto Dispatcher seleziona gli elementi di lavoro in base alla priorità ed esegue ciascuno fino al completamento. Ogni thread dell'interfaccia utente deve avere almeno un Dispatchere ognuno Dispatcher può eseguire elementi di lavoro in un solo thread.

Il trucco per creare applicazioni reattive e semplici da usare è ottimizzare la Dispatcher velocità effettiva mantenendo gli elementi di lavoro di piccole dimensioni. In questo modo gli elementi non vengono mai aggiornati nella coda in attesa dell'elaborazione Dispatcher . Qualsiasi ritardo percepibile tra input e risposta può causare frustrazione in un utente.

In che modo le applicazioni WPF dovrebbero gestire operazioni di grandi dimensioni? Cosa è necessario fare se il codice prevede calcoli complessi o l'esecuzione di query su un database in un server remoto? In genere, la risposta consiste nel gestire la grande operazione in un thread separato, lasciando libero il thread dell'interfaccia utente per tendenzialmente gli elementi nella Dispatcher coda. Al termine dell'operazione di grandi dimensioni, può segnalarne il risultato al thread dell'interfaccia utente per la visualizzazione.

Storicamente, Windows consente l'accesso agli elementi dell'interfaccia utente solo dal thread che li ha creati. Ciò significava che un thread in background responsabile di un'attività a esecuzione prolungata non poteva aggiornare una casella di testo dopo il suo completamento. Windows esegue questa operazione per garantire l'integrità dei componenti dell'interfaccia utente. Una casella di riepilogo può apparire strana se il relativo contenuto viene aggiornato da un thread in background mentre viene disegnata.

WPF dispone di un meccanismo di esclusione reciproca predefinito che applica questo coordinamento. La maggior parte delle classi in WPF deriva da DispatcherObject. In fase di costruzione, un archivia DispatcherObject un riferimento all'oggetto Dispatcher collegato al thread attualmente in esecuzione. In effetti, l'oggetto DispatcherObject viene associato al thread che lo crea. Durante l'esecuzione del programma, un DispatcherObject oggetto può chiamare il metodo pubblico VerifyAccess . VerifyAccess esamina l'oggetto Dispatcher associato al thread corrente e lo confronta con il riferimento archiviato durante la Dispatcher costruzione. Se non corrispondono, VerifyAccess genera un'eccezione. VerifyAccess deve essere chiamato all'inizio di ogni metodo appartenente a un oggetto DispatcherObject.

Se un solo thread può modificare l'interfaccia utente, come interagiscono i thread in background con l'utente? Un thread in background può chiedere al thread dell'interfaccia utente di eseguire un'operazione per suo conto. Questa operazione viene eseguita registrando un elemento di lavoro con l'oggetto Dispatcher del thread dell'interfaccia utente. La Dispatcher classe fornisce i metodi per la registrazione di elementi di lavoro: Dispatcher.InvokeAsync, Dispatcher.BeginInvokee Dispatcher.Invoke. Questi metodi pianificano un delegato per l'esecuzione. Invoke è una chiamata sincrona, ovvero non restituisce finché il thread dell'interfaccia utente non termina effettivamente l'esecuzione del delegato. InvokeAsync e BeginInvoke sono asincroni e restituiscono immediatamente.

Ordina Dispatcher gli elementi nella coda in base alla priorità. Durante l'aggiunta di un elemento alla Dispatcher coda possono essere specificati dieci livelli. Queste priorità vengono mantenute nell'enumerazione DispatcherPriority .

App a thread singolo con calcolo a esecuzione prolungata

La maggior parte delle interfacce utente grafiche (GUI) impiega una grande parte del tempo di inattività durante l'attesa di eventi generati in risposta alle interazioni dell'utente. Con un'attenta programmazione questo tempo di inattività può essere usato in modo costruttivo, senza influire sulla velocità di risposta dell'interfaccia utente. Il modello di threading WPF non consente all'input di interrompere un'operazione eseguita nel thread dell'interfaccia utente. Ciò significa che è necessario assicurarsi di tornare periodicamente all'elaborazione Dispatcher degli eventi di input in sospeso prima che vengano aggiornati.

Un'app di esempio che illustra i concetti di questa sezione può essere scaricata da GitHub per C# o Visual Basic.

Si consideri l'esempio seguente:

Screenshot that shows threading of prime numbers.

Questa semplice applicazione conta verso l'alto a partire da tre, cercando i numeri primi. Quando l'utente fa clic sul pulsante Start, viene avviata la ricerca. Quando il programma trova un numero primo, aggiorna l'interfaccia utente con il risultato trovato. In qualsiasi momento, l'utente può interrompere la ricerca.

Anche se è abbastanza semplice, la ricerca di numeri primi potrebbe continuare all'infinito e questo comporta alcune difficoltà. Se è stata gestita l'intera ricerca all'interno del gestore eventi click del pulsante, il thread dell'interfaccia utente non avrebbe mai la possibilità di gestire altri eventi. L'interfaccia utente non è in grado di rispondere ai messaggi di input o di elaborazione. L'interfaccia utente non verrebbe mai ridisegnata e i clic sui pulsanti non riceverebbero alcuna risposta.

È possibile eseguire la ricerca dei numeri primi in un thread separato, ma in questo caso sarebbe necessario affrontare problemi di sincronizzazione. Con un approccio a thread singolo, è possibile aggiornare direttamente l'etichetta che indica il numero primo più grande trovato.

Se si suddivide l'attività di calcolo in blocchi gestibili, è possibile tornare periodicamente agli Dispatcher eventi ed elaborarli. È possibile offrire a WPF l'opportunità di aggiornare ed elaborare l'input.

Il modo migliore per suddividere il tempo di elaborazione tra calcolo e gestione degli eventi consiste nel gestire il calcolo da Dispatcher. Usando il InvokeAsync metodo , è possibile pianificare i controlli dei numeri primi nella stessa coda da cui vengono estratti gli eventi dell'interfaccia utente. Nell'esempio viene pianificato il controllo di un singolo numero primo per volta. Al termine del controllo del numero primo, viene pianificato immediatamente il controllo successivo. Questo controllo procede solo dopo la gestione degli eventi dell'interfaccia utente in sospeso.

Screenshot that shows the dispatcher queue.

Microsoft Word esegue il controllo ortografico usando questo meccanismo. Il controllo ortografico viene eseguito in background usando il tempo di inattività del thread dell'interfaccia utente. Di seguito è riportato il codice.

L'esempio seguente mostra il codice XAML che crea l'interfaccia utente.

Importante

Il codice XAML illustrato in questo articolo proviene da un progetto C#. Xaml di Visual Basic è leggermente diverso quando si dichiara la classe di backup per 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'esempio seguente mostra il 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

Oltre ad aggiornare il testo in Button, il StartStopButton_Click gestore è responsabile della pianificazione del primo controllo del numero primo aggiungendo un delegato alla Dispatcher coda. Una volta completato il lavoro di questo gestore eventi, il Dispatcher delegato verrà selezionato per l'esecuzione.

Come accennato in precedenza, InvokeAsync è il Dispatcher membro usato per pianificare un delegato per l'esecuzione. In questo caso, scegliamo la SystemIdle priorità. Dispatcher Eseguirà questo delegato solo quando non sono presenti eventi importanti da elaborare. La velocità di risposta dell'interfaccia utente è più importante del controllo dei numeri. Viene anche passato un nuovo delegato che rappresenta la routine di controllo dei numeri.

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

Questo metodo controlla se il numero dispari successivo è un numero primo. Se è primo, il metodo aggiorna direttamente l'oggetto in modo che rifletta l'individuazione bigPrimeTextBlock . È possibile eseguire questa operazione perché il calcolo si verifica nello stesso thread usato per creare il controllo. Se si fosse scelto di usare un thread separato per il calcolo, sarebbe necessario usare un meccanismo di sincronizzazione più complesso ed eseguire l'aggiornamento nel thread dell'interfaccia utente. Questa situazione verrà illustrata di seguito.

Più finestre, più thread

Alcune applicazioni WPF richiedono più finestre di primo livello. È perfettamente accettabile per una combinazione thread/dispatcher per gestire più finestre, ma a volte diversi thread svolgono un lavoro migliore. Questo è particolarmente vero se c'è alcuna possibilità che una delle finestre monopolizzerà il thread.

Esplora risorse funziona in questo modo. Ogni nuova finestra di Esplora risorse appartiene al processo originale, ma viene creata sotto il controllo di un thread indipendente. Quando Explorer non risponde, ad esempio quando si cercano risorse di rete, altre finestre di Esplora risorse continuano a essere reattive e utilizzabili.

È possibile illustrare questo concetto con l'esempio seguente.

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.

Le prime tre finestre di questa immagine condividono lo stesso identificatore di thread: 1. Le due altre finestre hanno identificatori di thread diversi: Nine e 4. C'è un glifo color magenta ruotante ! !️ nella parte superiore destra di ogni finestra.

Questo esempio contiene una finestra con un glifo rotante ‼️ , un pulsante Sospendi e altri due pulsanti che creano una nuova finestra sotto il thread corrente o in un nuovo thread. Il ‼️ glifo viene ruotato costantemente fino a quando non viene premuto il pulsante Sospendi , che sospende il thread per cinque secondi. Nella parte inferiore della finestra viene visualizzato l'identificatore del thread.

Quando si preme il pulsante Sospendi , tutte le finestre sotto lo stesso thread diventano non rispondenti. Qualsiasi finestra in un thread diverso continua a funzionare normalmente.

L'esempio seguente è il codice XAML nella finestra:

<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'esempio seguente mostra il 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

Di seguito sono riportati alcuni dei dettagli da notare:

  • L'attività Task.Delay(TimeSpan) viene usata per fare in modo che il thread corrente venga sospeso per cinque secondi quando viene premuto il pulsante Sospendi .

    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
    
  • Il SameThreadWindow_Click gestore eventi visualizza in modo immediently una nuova finestra sotto il thread corrente. Il NewThreadWindow_Click gestore eventi crea un nuovo thread che avvia l'esecuzione del ThreadStartingPoint metodo , che a sua volta mostra una nuova finestra, come descritto nel punto punto elenco successivo.

    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
    
  • Il ThreadStartingPoint metodo è il punto di partenza per il nuovo thread. La nuova finestra viene creata sotto il controllo di questo thread. WPF crea automaticamente un nuovo System.Windows.Threading.Dispatcher oggetto per gestire il nuovo thread. Tutto quello che dobbiamo fare per rendere funzionale la finestra consiste nell'avviare .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'app di esempio che illustra i concetti di questa sezione può essere scaricata da GitHub per C# o Visual Basic.

Gestire un'operazione di blocco con Task.Run

La gestione delle operazioni di blocco in un'applicazione grafica può essere complessa. Non si vuole chiamare metodi di blocco dai gestori eventi perché l'applicazione sembra bloccarsi. L'esempio precedente ha creato nuove finestre nel proprio thread, consentendo l'esecuzione di ogni finestra indipendente l'una dall'altra. Anche se è possibile creare un nuovo thread con System.Windows.Threading.Dispatcher, diventa difficile sincronizzare il nuovo thread con il thread principale dell'interfaccia utente dopo il completamento del lavoro. Poiché il nuovo thread non può modificare direttamente l'interfaccia utente, è necessario usare Dispatcher.InvokeAsync, Dispatcher.BeginInvokeo Dispatcher.Invokeper inserire delegati nel del thread dell'interfaccia Dispatcher utente. Infine, questi delegati vengono eseguiti con l'autorizzazione per modificare gli elementi dell'interfaccia utente.

Esiste un modo più semplice per eseguire il codice in un nuovo thread durante la sincronizzazione dei risultati, ovvero il modello asincrono basato su attività (TAP). Si basa sui Task tipi e Task<TResult> nello spazio dei System.Threading.Tasks nomi , che vengono usati per rappresentare le operazioni asincrone. TAP usa un singolo metodo per rappresentare l'inizio e il completamento di un'operazione asincrona. Questo modello offre alcuni vantaggi:

  • Il chiamante di un Task può scegliere di eseguire il codice in modo asincrono o sincrono.
  • Lo stato di avanzamento può essere segnalato da Task.
  • Il codice chiamante può sospendere l'esecuzione e attendere il risultato dell'operazione.

Esempio task.run

In questo esempio viene simulata una chiamata RPC (Remote Procedure Call) che recupera i dati delle previsioni meteo. Quando si fa clic sul pulsante, l'interfaccia utente viene aggiornata per indicare che il recupero dei dati è in corso, mentre un'attività viene avviata per simulare il recupero delle previsioni meteo. All'avvio dell'attività, il codice del gestore eventi del pulsante viene sospeso fino al termine dell'attività. Al termine dell'attività, il codice del gestore eventi continua a essere eseguito. Il codice viene sospeso e non blocca il resto del thread dell'interfaccia utente. Il contesto di sincronizzazione di WPF gestisce la sospensione del codice, che consente l'esecuzione di WPF.

A diagram that demonstrates the workflow of the example app.

Diagramma che illustra il flusso di lavoro dell'app di esempio. L'app ha un singolo pulsante con il testo "Fetch Forecast". Dopo aver premuto il pulsante è presente una freccia che punta alla fase successiva dell'app, ovvero un'immagine dell'orologio posizionata al centro dell'app che indica che l'app è occupata nel recupero dei dati. Dopo qualche tempo, l'app torna con un'immagine del sole o delle nuvole di pioggia, a seconda del risultato dei dati.

Un'app di esempio che illustra i concetti di questa sezione può essere scaricata da GitHub per C# o Visual Basic. Il codice XAML per questo esempio è piuttosto grande e non è disponibile in questo articolo. Usare i collegamenti GitHub precedenti per esplorare il codice XAML. XAML usa un singolo pulsante per recuperare il meteo.

Prendere in considerazione il code-behind nel codice 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

Di seguito sono elencati alcuni dettagli da considerare.

  • Gestore eventi del pulsante

    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
    

    Si noti che il gestore eventi è stato dichiarato con async (o Async con Visual Basic). Un metodo "asincrono" consente la sospensione del codice quando viene chiamato un metodo atteso, ad esempio FetchWeatherFromServerAsync, . Questa proprietà è designata dalla await parola chiave (o Await con Visual Basic). Fino al FetchWeatherFromServerAsync termine, il codice del gestore del pulsante viene sospeso e il controllo viene restituito al chiamante. È simile a un metodo sincrono, ad eccezione del fatto che un metodo sincrono attende il completamento di ogni operazione nel metodo dopo il quale il controllo viene restituito al chiamante.

    I metodi attesi usano il contesto di threading del metodo corrente, che con il gestore dei pulsanti, è il thread dell'interfaccia utente. Ciò significa che la chiamata await FetchWeatherFromServerAsync(); (o Await FetchWeatherFromServerAsync() con Visual Basic) fa sì che il codice in FetchWeatherFromServerAsync venga eseguito nel thread dell'interfaccia utente, ma non viene eseguito nel dispatcher ha tempo per eseguirlo, in modo analogo al funzionamento dell'app a thread singolo con un esempio di calcolo a esecuzione prolungata. Si noti tuttavia che await Task.Run viene usato. Verrà creato un nuovo thread nel pool di thread per l'attività designata anziché per il thread corrente. Quindi FetchWeatherFromServerAsync viene eseguito sul proprio thread.

  • Recupero dei dati meteo

    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
    

    Per semplificare le operazioni, in questo esempio non è presente alcun codice di rete. Viene invece simulato il ritardo dell'accesso di rete sospendendo il thread per quattro secondi. In questa fase, il thread dell'interfaccia utente originale è ancora in esecuzione e risponde agli eventi dell'interfaccia utente mentre il gestore eventi del pulsante viene sospeso fino al completamento del nuovo thread. Per dimostrare questo problema, abbiamo lasciato un'animazione in esecuzione ed è possibile ridimensionare la finestra. Se il thread dell'interfaccia utente è stato sospeso o ritardato, l'animazione non viene visualizzata e non è possibile interagire con la finestra.

    Al termine, Task.Delay e abbiamo selezionato in modo casuale le previsioni meteo, lo stato meteo viene restituito al chiamante.

  • Aggiornamento dell'interfaccia utente

    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
    

    Al termine dell'attività e il thread dell'interfaccia utente ha tempo, il chiamante di Task.Run, il gestore eventi del pulsante, viene ripreso. Il resto del metodo arresta l'animazione dell'orologio e sceglie un'immagine per descrivere il meteo. Visualizza questa immagine e abilita il pulsante "fetch forecast".

Un'app di esempio che illustra i concetti di questa sezione può essere scaricata da GitHub per C# o Visual Basic.

Dettagli tecnici e punti a cascata

Le sezioni seguenti descrivono alcuni dettagli e punti a cascata che è possibile riscontrare con il multithreading.

Pompa annidata

A volte non è possibile bloccare completamente il thread dell'interfaccia utente. Si consideri il Show metodo della MessageBox classe . Show non restituisce finché l'utente non fa clic sul pulsante OK. Crea però una finestra che deve avere un ciclo di messaggi per essere interattiva. Mentre è in attesa del clic dell'utente su OK, la finestra dell'applicazione originale non risponde all'input utente. Continua però a elaborare i messaggi di disegno dell'interfaccia. La finestra originale ridisegna se stessa quando viene coperta e mostrata.

Screenshot that shows a MessageBox with an OK button

Un thread deve essere responsabile della finestra di messaggio. WPF potrebbe creare un nuovo thread solo per la finestra della finestra di messaggio, ma questo thread non è in grado di disegnare gli elementi disabilitati nella finestra originale (tenere presente la discussione precedente sull'esclusione reciproca). WPF usa invece un sistema di elaborazione dei messaggi annidato. La Dispatcher classe include un metodo speciale denominato PushFrame, che archivia il punto di esecuzione corrente di un'applicazione e quindi avvia un nuovo ciclo di messaggi. Al termine del ciclo di messaggi annidati, l'esecuzione riprende dopo la chiamata originale PushFrame .

In questo caso, PushFrame mantiene il contesto del programma alla chiamata a MessageBox.Showe avvia un nuovo ciclo di messaggi per ridipingere la finestra di sfondo e gestire l'input per la finestra della finestra della finestra di messaggio. Quando l'utente fa clic su OK e cancella la finestra popup, il ciclo annidato viene chiuso e il controllo riprende dopo la chiamata a Show.

Eventi indirizzati non aggiornati

Il sistema di eventi indirizzato in WPF invia una notifica a interi alberi quando vengono generati eventi.

<Canvas MouseLeftButtonDown="handler1" 
        Width="100"
        Height="100"
        >
  <Ellipse Width="50"
            Height="50"
            Fill="Blue" 
            Canvas.Left="30"
            Canvas.Top="50" 
            MouseLeftButtonDown="handler2"
            />
</Canvas>

Quando il pulsante sinistro del mouse viene premuto sull'ellisse, handler2 viene eseguito. Al handler2 termine, l'evento viene passato all'oggetto Canvas , che usa handler1 per elaborarlo. Ciò si verifica solo se handler2 non contrassegna in modo esplicito l'oggetto evento come gestito.

È possibile che handler2 l'elaborazione di questo evento richiederà molto tempo. handler2 può essere usato PushFrame per avviare un ciclo di messaggi annidato che non restituisce per ore. Se handler2 non contrassegna l'evento come gestito al termine di questo ciclo di messaggi, l'evento viene passato all'albero anche se è molto vecchio.

Reentrancy e blocco

Il meccanismo di blocco di Common Language Runtime (CLR) non si comporta esattamente come si potrebbe immaginare; ci si potrebbe aspettare che un thread interrompa completamente l'operazione quando si richiede un blocco. In realtà, il thread continua a ricevere ed elaborare i messaggi con priorità alta. Questo consente di evitare i deadlock e rendere le interfacce un minimo reattive, ma introduce la possibilità di bug. La maggior parte del tempo che non è necessario sapere nulla su questo, ma in rari casi (in genere che coinvolgono messaggi finestra Win32 o componenti COM STA) questo può essere la pena conoscere.

La maggior parte delle interfacce non viene compilata tenendo conto della thread safety perché gli sviluppatori si basano sul presupposto che un'interfaccia utente non sia mai accessibile da più thread. In questo caso, tale thread singolo può apportare modifiche ambientali in momenti imprevisti, causando tali effetti negativi che il DispatcherObject meccanismo di esclusione reciproca dovrebbe risolvere. Si consideri lo pseudocodice seguente:

Diagram that shows threading reentrancy.

La maggior parte del tempo è la cosa giusta, ma in WPF ci sono momenti in cui tale reentrancy imprevista può effettivamente causare problemi. Pertanto, in determinati momenti chiave WPF chiama DisableProcessing, che modifica l'istruzione di blocco per il thread in modo da usare il blocco senza reentrancy WPF, invece del normale blocco CLR.

Perché il team CLR ha scelto questo comportamento? La scelta ha a che fare con gli oggetti COM STA e il thread di finalizzazione. Quando un oggetto viene sottoposto a Garbage Collection, il relativo Finalize metodo viene eseguito sul thread finalizzatore dedicato, non sul thread dell'interfaccia utente. E qui risiede il problema, perché un oggetto COM STA creato nel thread dell'interfaccia utente può essere eliminato solo sul thread dell'interfaccia utente. CLR esegue l'equivalente di un oggetto BeginInvoke (in questo caso usando Win32).SendMessage Tuttavia, se il thread dell'interfaccia utente è occupato, il thread del finalizzatore viene bloccato e l'oggetto STA COM non può essere eliminato, creando una grave perdita di memoria. Quindi il team CLR ha fatto la difficile chiamata a fare blocchi il modo in cui funzionano.

L'attività per WPF consiste nell'evitare la reentrancy imprevista senza reintrodurre la perdita di memoria, motivo per cui non si blocca la reentrancy ovunque.

Vedi anche