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:
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.
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.
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 MessageBox. Show 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.
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.
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