Modello di threading

Windows Presentation Foundation (WPF) è progettato per evitare agli sviluppatori le difficoltà del threading. Come risultato, alla maggior parte degli sviluppatori WPF non è richiesta la scrittura di un'interfaccia che utilizza più thread. Poiché i programmi multithreading sono complessi ed è difficile eseguirne il debug, è opportuno evitarsi se sono presenti soluzioni a thread singolo.

Indipendentemente dall'architettura, tuttavia, nessun framework UI sarà mai in grado di fornire una soluzione a thread singolo per ogni tipo di problema. WPF offre sicuramente soluzioni a thread singolo per un numero maggiore di problemi ma esistono ancora situazioni in cui più thread migliorano la velocità di risposta dell'user interface (UI) o le prestazioni dell'applicazione. Dopo avere illustrato alcune nozioni di base, in questo documento si intende analizzare alcune di queste situazioni e concludere descrivendo alcuni dettagli di livello inferiore.

Nel presente argomento sono contenute le seguenti sezioni.

  • Cenni preliminari e dispatcher
  • Thread in azione: esempi
  • Dettagli tecnici e difficoltà
  • Argomenti correlati

Cenni preliminari e dispatcher

Le applicazioni WPF vengono in genere avviate con due thread: uno per la gestione del rendering e un altro per la gestione dell'UI. Il thread di rendering viene eseguito in background in modo efficiente, mentre il thread dell'UI riceve l'input, gestisce gli eventi, aggiorna la visualizzazione sullo schermo ed esegue il codice dell'applicazione. La maggior parte delle applicazioni utilizza un singolo thread dell'UI, sebbene in alcune situazioni sia preferibile utilizzare il multithreading. Di seguito verrà fornito un esempio.

Il thread dell'UI accoda elementi di lavoro in un oggetto denominato Dispatcher. L'oggetto Dispatcher seleziona gli elementi di lavoro in base alle priorità e li esegue singolarmente fino al completamento. Ogni thread UI deve presentare almeno un oggetto Dispatcher e ogni oggetto Dispatcher può eseguire gli elementi di lavoro esattamente in un thread.

La soluzione per compilare applicazioni reattive e di utilizzo intuitivo consiste nell'ottimizzare le prestazioni dell'oggetto Dispatcher contenendo le dimensioni degli elementi di lavoro. In questo modo, gli elementi non rimarranno mai statici nella coda dell'oggetto Dispatcher nell'attesa di essere elaborati. Qualsiasi ritardo percepibile tra input e risposta può causare frustrazione all'utente.

In che modo, quindi, le applicazioni WPF gestiscono le operazioni più complesse? E se il codice comportasse l'esecuzione di un calcolo esteso o la necessità di eseguire query in un database installato in un server remoto? La risposta prevede in genere la gestione dell'operazione in un thread separato in modo che il thread dell'UI rimanga riservato agli elementi accodati nell'oggetto Dispatcher. Al termine dell'operazione, il relativo risultato potrà essere rimandato al thread UI affinché venga visualizzato.

Storicamente, in Windows agli elementi dell'UI può accedere solo il thread che li ha creati. Ciò significa che un thread in background responsabile dell'esecuzione di un'attività di lunga durata non può aggiornare una casella di testo al suo completamento. In questo modo, è possibile per Windows garantire l'integrità dei componenti dell'UI. Una casella di riepilogo potrebbe assumere un aspetto strano se il relativo contenuto venisse aggiornato da un thread in background durante l'aggiornamento dello schermo.

WPF dispone di un meccanismo di esclusione reciproca incorporato che impone questa coordinazione. La maggior parte delle classi in WPF deriva dall'oggetto DispatcherObject. In fase di costruzione, in un oggetto DispatcherObject viene archiviato un riferimento all'oggetto Dispatcher collegato al thread in esecuzione. Di fatto, l'oggetto DispatcherObject viene associato al thread che lo ha creato. Durante l'esecuzione del programma, un oggetto DispatcherObject può chiamare il relativo metodo VerifyAccess pubblico. VerifyAccess esamina l'oggetto Dispatcher associato al thread corrente e lo confronta con il riferimento all'oggetto Dispatcher archiviato durante la costruzione. Se non corrispondono, VerifyAccess genera un'eccezione. VerifyAccess deve essere chiamato all'inizio di ogni metodo appartenente a un oggetto DispatcherObject.

Se solo un thread può modificare l'UI, come interagiscono con l'utente i thread in background? Un thread in background può chiedere al thread dell'UI di eseguire un'operazione per suo conto. A tale scopo, registra un elemento di lavoro con l'oggetto Dispatcher del thread dell'UI. Nella classe Dispatcher sono disponibili due metodi per la registrazione degli elementi di lavoro: Invoke e BeginInvoke. Entrambi i metodi pianificano un delegato per l'esecuzione. Il metodo Invoke consiste in una chiamata sincrona, ovvero non restituisce un risultato finché il thread dell' UI non termina l'esecuzione del delegato. BeginInvoke è invece asincrono e restituisce immediatamente un risultato.

L'oggetto Dispatcher ordina gli elementi nella coda in base alla priorità. Quando si aggiunge un elemento alla coda dell'oggetto Dispatcher è possibile specificare dieci livelli. Tali priorità vengono mantenute nell'enumerazione DispatcherPriority. Per informazioni dettagliate sui livelli DispatcherPriority, vedere la documentazione di Windows SDK.

Thread in azione: esempi

Applicazione a thread singolo con un calcolo di lunga durata

La maggior parte delle graphical user interfaces (GUIs) deve rimanere a lungo in attesa degli eventi generati in risposta alle interazioni degli utenti. on un'attenta programmazione è possibile utilizzare questo tempo in modo costruttivo, senza influire negativamente sulla velocità di risposta dell'UI. Il modello di threading di WPF non consente all'input di interrompere un'operazione in corso nel thread dell'UI. Ciò significa che sarà necessario tornare periodicamente all'oggetto Dispatcher per elaborare gli eventi di input in sospeso prima che non siano più aggiornati.

Si consideri l'esempio seguente:

Schermata di numeri primi

Questa semplice applicazione conta da tre in avanti e cerca numeri primi. Quando l'utente fa clic sul pulsante Start, ha inizio la ricerca. Quando viene trovato un numero primo, l'interfaccia utente viene aggiornata di conseguenza. L'utente può interrompere la ricerca in qualsiasi momento.

Nonostante la sua semplicità, la ricerca dei numeri primi potrebbe continuare all'infinito e comportare quindi alcuni problemi. Se l'intera ricerca venisse gestita all'interno del gestore dell'evento Click del pulsante, il thread dell'UI non avrebbe alcuna possibilità di gestire altri eventi. L'UI non sarebbe quindi in grado di rispondere all'input o elaborare messaggi. Analogamente, non verrebbe mai aggiornata e non risponderebbe mai ai clic sui pulsanti.

La ricerca dei numeri primi potrebbe essere eseguita in un thread separato, ma in tal caso sorgerebbero problemi di sincronizzazione. Con un approccio a thread singolo, è possibile aggiornare direttamente l'etichetta che elenca il numero primo più elevato trovato.

Scomponendo l'attività di calcolo in blocchi gestibili, è possibile tornare periodicamente all'oggetto Dispatcher ed elaborare gli eventi. In questo modo, WPF avrà la possibilità di aggiornare lo schermo ed elaborare l'input.

Il modo migliore di dividere il tempo di elaborazione tra calcolo e gestione degli eventi consiste nel gestire il calcolo dall'oggetto Dispatcher. Tramite il metodo BeginInvoke è possibile pianificare le ricerche dei numeri primi nella stessa coda da cui provengono gli eventi dell'UI. Nell'esempio, viene pianificata una sola ricerca di numeri primi alla volta. Al termine di una ricerca, viene immediatamente pianificata quella successiva. La ricerca procede solo dopo che gli eventi dell'UI in sospeso sono stati gestiti.

Illustrazione della coda del dispatcher

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

Nell'esempio riportato di seguito viene illustrata la creazione dell'interfaccia utente tramite XAML.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>
<Window x:Class="SDKSamples.MainWindow"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
        <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
        <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
    </StackPanel>
</Window>

Nell'esempio riportato di seguito viene illustrato il code-behind.

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class MainWindow
        Inherits Window
        Public Delegate Sub NextPrimeDelegate()

        'Current number to check 
        Private num As Long = 3

        Private continueCalculating As Boolean = False

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
            If continueCalculating Then
                continueCalculating = False
                startStopButton.Content = "Resume"
            Else
                continueCalculating = True
                startStopButton.Content = "Stop"
                startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
            End If
        End Sub

        Public Sub CheckNextNumber()
            ' Reset flag.
            NotAPrime = False

            For i As Long = 3 To Math.Sqrt(num)
                If num Mod i = 0 Then
                    ' Set not a prime flag to true.
                    NotAPrime = True
                    Exit For
                End If
            Next

            ' If a prime number.
            If Not NotAPrime Then
                bigPrime.Text = num.ToString()
            End If

            num += 2
            If continueCalculating Then
                startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
            End If
        End Sub

        Private NotAPrime As Boolean = False
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        public delegate void NextPrimeDelegate();

        //Current number to check 
        private long num = 3;   

        private bool continueCalculating = false;

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void StartOrStop(object sender, EventArgs e)
        {
            if (continueCalculating)
            {
                continueCalculating = false;
                startStopButton.Content = "Resume";
            }
            else
            {
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
            }
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            NotAPrime = false;

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

            // If a prime number.
            if (!NotAPrime)
            {
                bigPrime.Text = num.ToString();
            }

            num += 2;
            if (continueCalculating)
            {
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle, 
                    new NextPrimeDelegate(this.CheckNextNumber));
            }
        }

        private bool NotAPrime = false;
    }
}

Nell'esempio riportato di seguito viene illustrato il gestore eventi per Button.

Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
    If continueCalculating Then
        continueCalculating = False
        startStopButton.Content = "Resume"
    Else
        continueCalculating = True
        startStopButton.Content = "Stop"
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
    End If
End Sub
private void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

Oltre ad aggiornare il testo sull'oggetto Button, questo gestore è responsabile della pianificazione della prima ricerca di numeri primi aggiungendo un delegato alla coda dell'oggetto Dispatcher. Talvolta, al termine delle operazioni eseguite dal gestore eventi, il delegato viene selezionato dall'oggetto Dispatcher per l'esecuzione.

Come indicato in precedenza, BeginInvoke è il membro Dispatcher utilizzato per pianificare un delegato per l'esecuzione. In questo caso, viene scelta la priorità SystemIdle. Il delegato verrà eseguito dall'oggetto Dispatcher solo se non vi sono eventi importanti da elaborare. La velocità di risposta dell'UI è più importante della ricerca di numeri. Viene inoltre passato un nuovo delegato per la rappresentazione della routine di ricerca dei numeri.

Public Sub CheckNextNumber()
    ' Reset flag.
    NotAPrime = False

    For i As Long = 3 To Math.Sqrt(num)
        If num Mod i = 0 Then
            ' Set not a prime flag to true.
            NotAPrime = True
            Exit For
        End If
    Next

    ' If a prime number.
    If Not NotAPrime Then
        bigPrime.Text = num.ToString()
    End If

    num += 2
    If continueCalculating Then
        startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
    End If
End Sub

Private NotAPrime As Boolean = False
public void CheckNextNumber()
{
    // Reset flag.
    NotAPrime = false;

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

    // If a prime number.
    if (!NotAPrime)
    {
        bigPrime.Text = num.ToString();
    }

    num += 2;
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle, 
            new NextPrimeDelegate(this.CheckNextNumber));
    }
}

private bool NotAPrime = false;

Questo metodo consente di verificare se il numero dispari successivo è un numero primo. Se è un numero primo, il metodo aggiorna direttamente l'oggetto bigPrimeTextBlock di conseguenza. Questa operazione è possibile perché il calcolo viene eseguito nello stesso thread utilizzato per creare il componente. Se il calcolo fosse stato eseguito in un thread separato, sarebbe stato necessario utilizzare un meccanismo di sincronizzazione più complesso ed eseguire l'aggiornamento nel thread dell'UI. Questa situazione verrà illustrata in seguito.

Per il codice sorgente completo di questo esempio, vedere Esempio di applicazione a thread singolo con calcolo di lunga durata (la pagina potrebbe essere in inglese).

Gestione di un'operazione di blocco con un thread in background

La gestione delle operazioni di blocco in un'applicazione grafica può rivelarsi difficile. È sconsigliabile chiamare metodi di blocco da gestori eventi perché si causerebbe il blocco dell'applicazione. È possibile utilizzare un thread separato per gestire queste operazioni, ma al termine sarà necessario eseguire la sincronizzazione con il thread dell'UI perché non è possibile modificare direttamente la GUI dal thread di lavoro. È possibile utilizzare Invoke o BeginInvoke per inserire delegati nell'oggetto Dispatcher del thread dell'UI. Questi delegati verranno eseguiti con l'autorizzazione a modificare gli elementi dell'UI.

In questo esempio viene simulata una RPC (remote procedure call) per il recupero di previsioni meteorologiche. Per eseguire questa chiamata viene utilizzato un thread di lavoro separato e al termine viene pianificato un metodo di aggiornamento nell'oggetto Dispatcher del thread dell'UI.

Schermata dell'interfaccia sulle previsioni del tempo


Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window
        ' Delegates to be used in placking jobs onto the Dispatcher.
        Private Delegate Sub NoArgDelegate()
        Private Delegate Sub OneArgDelegate(ByVal arg As String)

        ' Storyboards for the animations.
        Private showClockFaceStoryboard As Storyboard
        Private hideClockFaceStoryboard As Storyboard
        Private showWeatherImageStoryboard As Storyboard
        Private hideWeatherImageStoryboard As Storyboard

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Load the storyboard resources.
            showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
            hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
            showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
            hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
        End Sub

        Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Change the status image and start the rotation animation.
            fetchButton.IsEnabled = False
            fetchButton.Content = "Contacting Server"
            weatherText.Text = ""
            hideWeatherImageStoryboard.Begin(Me)

            ' Start fetching the weather forecast asynchronously.
            Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)

            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub FetchWeatherFromServer()
            ' Simulate the delay from network access.
            Thread.Sleep(4000)

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

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

            ' Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
        End Sub

        Private Sub UpdateUserInterface(ByVal weather As String)
            'Set the weather image
            If weather = "sunny" Then
                weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
            ElseIf weather = "rainy" Then
                weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
            End If

            'Stop clock animation
            showClockFaceStoryboard.Stop(Me)
            hideClockFaceStoryboard.Begin(Me)

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

        Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showWeatherImageStoryboard.Begin(Me)
        End Sub

        Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showClockFaceStoryboard.Begin(Me, True)
        End Sub
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);

            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              

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

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

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }

        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }
}

Di seguito sono riportati alcuni dei dettagli più importanti.

  • Creazione del gestore di pulsanti

            Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
                ' Change the status image and start the rotation animation.
                fetchButton.IsEnabled = False
                fetchButton.Content = "Contacting Server"
                weatherText.Text = ""
                hideWeatherImageStoryboard.Begin(Me)
    
                ' Start fetching the weather forecast asynchronously.
                Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
    
                fetcher.BeginInvoke(Nothing, Nothing)
            End Sub
    
    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
    
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
    
        fetcher.BeginInvoke(null, null);
    }
    

Quando si fa clic sul pulsante, viene visualizzato il disegno dell'orologio e ne viene avviata l'animazione. Disabilitare il pulsante. Viene chiamato il metodo FetchWeatherFromServer in un nuovo thread e viene quindi restituito un risultato, consentendo all'oggetto Dispatcher di elaborare eventi nell'attesa di raccogliere le previsioni meteorologiche.

  • Recupero delle previsioni meteorologiche

            Private Sub FetchWeatherFromServer()
                ' Simulate the delay from network access.
                Thread.Sleep(4000)
    
                ' Tried and true method for weather forecasting - random numbers.
                Dim rand As New Random()
                Dim weather As String
    
                If rand.Next(2) = 0 Then
                    weather = "rainy"
                Else
                    weather = "sunny"
                End If
    
                ' Schedule the update function in the UI thread.
                tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
            End Sub
    
    private void FetchWeatherFromServer()
    {
        // Simulate the delay from network access.
        Thread.Sleep(4000);              
    
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
    
        if (rand.Next(2) == 0)
        {
            weather = "rainy";
        }
        else
        {
            weather = "sunny";
        }
    
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface), 
            weather);
    }
    

Per evitare complicazioni, questo esempio non prevede alcun codice di rete. Viene invece simulato il ritardo dell'accesso alla rete rendendo inattivo il nuovo thread per quattro secondi. Durante questo periodo di tempo, il thread dell'UI originale è ancora in esecuzione e risponde agli eventi. Per illustrare questa situazione, l'animazione viene lasciata in esecuzione e i pulsanti di ingrandimento e riduzione a icona rimangono funzionanti.

Al termine del ritardo, dopo aver selezionato in modo casuale la previsione meteorologica, è necessario comunicarne il risultato al thread dell'UI. A tal fine viene pianificata una chiamata a UpdateUserInterface nel thread dell'UI utilizzando l'oggetto Dispatcher del thread. Viene passata una stringa che descrive le condizioni meteorologiche a questa chiamata al metodo pianificata.

  • Aggiornamento dell'UI

            Private Sub UpdateUserInterface(ByVal weather As String)
                'Set the weather image
                If weather = "sunny" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
                ElseIf weather = "rainy" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
                End If
    
                'Stop clock animation
                showClockFaceStoryboard.Stop(Me)
                hideClockFaceStoryboard.Begin(Me)
    
                'Update UI text
                fetchButton.IsEnabled = True
                fetchButton.Content = "Fetch Forecast"
                weatherText.Text = weather
            End Sub
    
    private void UpdateUserInterface(String weather)
    {    
        //Set the weather image
        if (weather == "sunny")
        {       
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        }
        else if (weather == "rainy")
        {
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        }
    
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
    
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;     
    }
    

Quando vi è tempo a disposizione, l'oggetto Dispatcher nel thread dell'UI esegue una chiamata pianificata a UpdateUserInterface. Questo metodo arresta l'animazione dell'orologio e sceglie un'immagine per descrivere le condizioni meteorologiche. Visualizza questa immagine e ripristina il pulsante "fetch forecast".

Più finestre e più thread

Alcune applicazioni WPF richiedono più finestre di livello principale. È accettabile che una combinazione di Thread/Dispatcher gestisca più finestre, ma talvolta più thread rappresentano una soluzione più efficiente, soprattutto se esiste una qualsiasi possibilità che una delle finestre monopolizzi 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.

Utilizzando un controllo Frame WPF, è possibile visualizzare pagine Web. È possibile creare con facilità un semplice sostituto di Internet Explorer. Si inizia con un'importante funzionalità: la possibilità di aprire una nuova finestra di Esplora risorse. Quando l'utente fa clic sul pulsante "new window", viene avviata una copia della finestra in un thread separato. In questo modo, le operazioni di blocco o di lunga durata eseguite in una delle finestre non bloccheranno tutte le altre.

In realtà, il modello del browser dispone di un proprio modello di threading complicato. Tale modello è stato scelto perché dovrebbe essere noto alla maggior parte dei lettori.

Nell'esempio riportato di seguito viene illustrato il codice.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="MultiBrowse"
    Height="600" 
    Width="800"
    Loaded="OnLoaded"
    >
  <StackPanel Name="Stack" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
      <Button Content="New Window"
              Click="NewWindowHandler" />
      <TextBox Name="newLocation"
               Width="500" />
      <Button Content="GO!"
              Click="Browse" />
    </StackPanel>

    <Frame Name="placeHolder"
            Width="800"
            Height="550"></Frame>
  </StackPanel>
</Window>

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading


Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
           placeHolder.Source = New Uri("https://www.msn.com")
        End Sub

        Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
            placeHolder.Source = New Uri(newLocation.Text)
        End Sub

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

        Private Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;


namespace SDKSamples
{
    public partial class Window1 : Window
    {

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
           placeHolder.Source = new Uri("https://www.msn.com");
        }

        private void Browse(object sender, RoutedEventArgs e)
        {
            placeHolder.Source = new Uri(newLocation.Text);
        }

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

        private void ThreadStartingPoint()
        {
            Window1 tempWindow = new Window1();
            tempWindow.Show();       
            System.Windows.Threading.Dispatcher.Run();
        }
    }
}

I segmenti di threading riportati di seguito di questo codice sono i più interessanti in questo contesto:

        Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub
private void NewWindowHandler(object sender, RoutedEventArgs e)
{       
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}

Questo metodo viene chiamato quando l'utente fa clic sul pulsante "new window". Viene creato un nuovo thread che viene avviato in modo asincrono.

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

Questo metodo è il punto di partenza del nuovo thread. Viene creata una nuova finestra sotto il controllo di questo thread. WPF crea automaticamente un nuovo oggetto Dispatcher per la gestione del nuovo thread. Per rendere funzionale la finestra, è necessario avviare l'oggetto Dispatcher.

Dettagli tecnici e difficoltà

Scrittura di componenti utilizzando il threading

Nella Guida per gli sviluppatori di Microsoft .NET Frameworkviene descritto un modello in base al quale un componente può esporre il comportamento asincrono ai relativi client (vedere Cenni preliminari sul modello asincrono basato su eventi). Si supponga, ad esempio, di desiderare di comprimere il metodo FetchWeatherFromServer in un componente riutilizzabile e non grafico. In base al modello di Microsoft .NET Framework standard, il componente dovrebbe avere il seguente aspetto.

    Public Class WeatherComponent
        Inherits Component
        'gets weather: Synchronous 
        Public Function GetWeather() As String
            Dim weather As String = ""

            'predict the weather

            Return weather
        End Function

        'get weather: Asynchronous 
        Public Sub GetWeatherAsync()
            'get the weather
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
    End Class

    Public Class GetWeatherCompletedEventArgs
        Inherits AsyncCompletedEventArgs
        Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
            MyBase.New([error], canceled, userState)
            _weather = weather
        End Sub

        Public ReadOnly Property Weather() As String
            Get
                Return _weather
            End Get
        End Property
        Private _weather As String
    End Class

    Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
public class WeatherComponent : Component
{
    //gets weather: Synchronous 
    public string GetWeather()
    {
        string weather = "";

        //predict the weather

        return weather;
    }

    //get weather: Asynchronous 
    public void GetWeatherAsync()
    {
        //get the weather
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}

public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
    public GetWeatherCompletedEventArgs(Exception error, bool canceled,
        object userState, string weather)
        :
        base(error, canceled, userState)
    {
        _weather = weather;
    }

    public string Weather
    {
        get { return _weather; }
    }
    private string _weather;
}

public delegate void GetWeatherCompletedEventHandler(object sender,
    GetWeatherCompletedEventArgs e);

GetWeatherAsync utilizzerebbe una delle tecniche descritte in precedenza, ad esempio la creazione di un thread in background, per funzionare in modo asincrono per non bloccare il thread chiamante.

Una delle parti più importanti del pattern consiste nel chiamare il metodo MethodNameCompleted sullo stesso thread in cui è stato chiamato il metodo MethodNameAsync. È possibile eseguire questa operazione abbastanza facilmente utilizzando WPF, archiviando CurrentDispatcher, ma in questo caso il componente non grafico potrebbe essere utilizzato solo nelle applicazioni WPF, non in Windows Forms o programmi ASP.NET.

La classe DispatcherSynchronizationContext risponde a questa esigenza, si pensi ad essa come a una versione semplificata dell'oggetto Dispatcher utilizzabile anche con altri framework dell'UI.

    Public Class WeatherComponent2
        Inherits Component
        Public Function GetWeather() As String
            Return fetchWeatherFromServer()
        End Function

        Private requestingContext As DispatcherSynchronizationContext = Nothing

        Public Sub GetWeatherAsync()
            If requestingContext IsNot Nothing Then
                Throw New InvalidOperationException("This component can only handle 1 async request at a time")
            End If

            requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)

            Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)

            ' Launch thread
            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
            RaiseEvent GetWeatherCompleted(Me, e)
        End Sub

        Private Function fetchWeatherFromServer() As String
            ' do stuff
            Dim weather As String = ""

            Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)

            Dim callback As New SendOrPostCallback(AddressOf DoEvent)
            requestingContext.Post(callback, e)
            requestingContext = Nothing

            Return e.Weather
        End Function

        Private Sub DoEvent(ByVal e As Object)
            'do stuff
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
        Public Delegate Function NoArgDelegate() As String
    End Class
public class WeatherComponent2 : Component
{
    public string GetWeather()
    {
        return fetchWeatherFromServer();
    }

    private DispatcherSynchronizationContext requestingContext = null;

    public void GetWeatherAsync()
    {
        if (requestingContext != null)
            throw new InvalidOperationException("This component can only handle 1 async request at a time");

        requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;

        NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);

        // Launch thread
        fetcher.BeginInvoke(null, null);
    }

    private void RaiseEvent(GetWeatherCompletedEventArgs e)
    {
        if (GetWeatherCompleted != null)
            GetWeatherCompleted(this, e);
    }

    private string fetchWeatherFromServer()
    {
        // do stuff
        string weather = "";

        GetWeatherCompletedEventArgs e =
            new GetWeatherCompletedEventArgs(null, false, null, weather);

        SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
        requestingContext.Post(callback, e);
        requestingContext = null;

        return e.Weather;
    }

    private void DoEvent(object e)
    {
        //do stuff
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public delegate string NoArgDelegate();
}

Distribuzione annidata

Talvolta non è fattibile bloccare completamente il thread dell'UI. Si consideri il metodo Show della classe MessageBoxShow non restituisce un risultato finché l'utente non fa clic sul pulsante OK. Crea invece una finestra che per essere interattiva deve presentare un ciclo di messaggi. Prima che l'utente faccia clic sul pulsante OK, la finestra originale dell'applicazione non risponde all'input dell'utente. I messaggi relativi alle operazioni di disegno vengono invece elaborati. La finestra originale viene ridisegnata quando viene nascosta e quindi nuovamente visualizzata. 

MessageBox con pulsante "OK"

Un thread deve essere responsabile della finestra di messaggio. In WPF viene creato un nuovo thread riservato alla finestra di messaggio, ma tale thread non è in grado di disegnare gli elementi disabilitati nella finestra originale (a questo proposito, fare riferimento alla sezione relativa all'esclusione reciproca). In WPF viene invece utilizzato un sistema di elaborazione dei messaggi annidato. Nella classe Dispatcher è incluso un metodo speciale denominato PushFrame che archivia il punto di esecuzione corrente di un'applicazione, dopodiché inizia un nuovo ciclo di messaggi. Al termine del ciclo di messaggi annidati, l'esecuzione riprende dopo la chiamata al metodo PushFrame originale.

In questo caso, il metodo PushFrame mantiene il contesto di programma a livello della chiamata a MessageBox.Show e avvia un nuovo ciclo di messaggi per ridisegnare la finestra di sfondo e gestire l'input alla finestra di messaggio. Quando l'utente fa clic sul pulsante OK e cancella la finestra popup, il ciclo annidato viene interrotto e il controllo riprende dopo la chiamata al metodo Show.

Eventi indirizzati non aggiornati

Il sistema di eventi indirizzati di WPF notifica intere strutture ad albero 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 viene premuto il pulsante sinistro del mouse sull'ellisse, viene eseguito handler2. Al termine di handler2, l'evento viene passato all'oggetto Canvas che utilizza handler1 per elaborarlo. Ciò si verifica solo se handler2 non contrassegna in modo esplicito l'oggetto evento come gestito.

È possibile che l'elaborazione di questo evento richieda molto tempo da parte di handler2. handler2 potrebbe utilizzare PushFrame per avviare un ciclo di messaggi annidati che non restituisce risultati per ore. Se handler2 non contrassegna l'evento come gestito al completamento di questo ciclo di messaggi, l'evento viene passato alla struttura ad albero sebbene sia molto obsoleto.

Reentrancy e blocco

Il comportamento del meccanismo di blocco di common language runtime (CLR) non è prevedibile; ci si aspetterebbe infatti che un thread interrompa completamente le operazioni in caso di richiesta di un blocco. In realtà, il thread continua a ricevere e a elaborare i messaggi con priorità alta. In questo modo, si evitano i deadlock e si riduce la velocità di risposta delle interfacce, ma si introduce la possibilità di bug di piccola entità. Nella maggior parte dei casi non è necessario possedere questo tipo di conoscenze, ma in rare circostanze, in genere nell'ambito dei componenti STA COM o dei messaggi delle finestre di Win32, possono rivelarsi molto utili.

La maggior parte delle interfacce non viene compilata tenendo presente la sicurezza dei thread perché gli sviluppatori si basano sul presupposto che a un'UI non possano accedere più thread. In questo caso, un tale singolo thread può apportare modifiche ambientali in modo del tutto imprevisto, causando i problemi che il meccanismo di esclusione reciproca dell'oggetto DispatcherObject dovrebbe risolvere. Si consideri il seguente pseudocodice.

Diagramma della reentrancy del threading

Nella maggior parte dei casi questa è la strada giusta da percorrere, tuttavia in altri questa reentrancy imprevista può causare seri problemi in WPF. Pertanto, in alcune circostanze chiave, WPF chiama l'oggetto DisableProcessing per modificare l'istruzione di blocco del thread affinché venga utilizzato il blocco senza reentrancy di WPF anziché il solito blocco di CLR. 

Perché il team dei tecnici di CLR ha scelto questo comportamento? Aveva a che fare con gli oggetti STA COM e il thread di finalizzazione. Quando un oggetto viene raccolto nel Garbage Collector, il relativo metodo Finalize viene eseguito sul thread del finalizzatore dedicato, non sul thread dell'UI. Ed è qui che sorge il problema, perché un oggetto STA COM creato sul thread dell' UI può essere eliminato solo sul thread dell'UI. CLR esegue funzioni equivalenti a un oggetto BeginInvoke (in questo caso l'utilizzo di SendMessage di Win32). Tuttavia, se il thread dell'UI è occupato, il thread del finalizzatore si blocca e l'oggetto STA COM non può essere eliminato. Il risultato è una seria perdita di memoria. Il team dei tecnici di CLR ha eseguito la chiamata necessaria per ottenere questo funzionamento dei blocchi.  

L'attività di WPF consiste nell'evitare una reentrancy imprevista senza causare perdita di memoria. Ecco il motivo per cui la reentrancy non viene bloccata ovunque.

Vedere anche

Altre risorse

Esempio di applicazione a thread singolo con calcolo di lunga durata