Condividi tramite


Modello di threading

Aggiornamento: novembre 2007

Windows Presentation Foundation (WPF) è stato progettato per semplificare il threading. Di conseguenza, la maggior parte degli sviluppatori di WPF non dovrà scrivere un'interfaccia che utilizza più di un thread. Poiché i programmi multithreading sono complessi e il loro debug risulta di difficile esecuzione, è opportuno evitarli in presenza di soluzioni a thread singolo.

Indipendentemente dall'architettura, tuttavia, nessun framework dell'interfaccia utente 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'interfaccia utente 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'interfaccia utente. Il thread di rendering viene eseguito in background in modo efficiente, mentre il thread dell'interfaccia utente 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'interfaccia utente, sebbene in alcune situazioni sia preferibile utilizzare il multithreading. Di seguito verrà fornito un esempio.

Il thread dell'interfaccia utente accoda elementi di lavoro in un oggetto denominato Dispatcher. L'oggetto Dispatcher seleziona gli elementi di lavoro in base alla priorità e li esegue singolarmente fino al completamento. Ogni thread dell'interfaccia utente 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'interfaccia utente rimanga riservato agli elementi accodati nell'oggetto Dispatcher. Al termine dell'operazione, il relativo risultato potrà essere rimandato al thread dell'interfaccia utente affinché venga visualizzato.

Storicamente, in Windows agli elementi dell'interfaccia utente 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'interfaccia utente. 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 pubblico VerifyAccess. 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'interfaccia utente, come interagiscono con l'utente i thread in background? Un thread in background può chiedere al thread dell'interfaccia utente di eseguire un'operazione per suo conto. A tale scopo, registra un elemento di lavoro con l'oggetto Dispatcher del thread dell'interfaccia utente. 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'interfaccia utente non termina l'esecuzione del delegato. Il metodo 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 interfacce utente grafiche (GUI, Graphical User Interfaces) deve rimanere a lungo in attesa degli eventi generati in risposta alle interazioni degli utenti. Con un'attenta programmazione è possibile utilizzare questo tempo in modo costruttivo, senza influire negativamente sulla velocità di risposta dell'interfaccia utente. Il modello di threading di WPF non consente all'input di interrompere un'operazione in corso nel thread dell'interfaccia utente. 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'interfaccia utente non avrebbe alcuna possibilità di gestire altri eventi. L'interfaccia utente 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'interfaccia utente. 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'interfaccia utente 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'interfaccia utente. 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>

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

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

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

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

        private bool continueCalculating = false;

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

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

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

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

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

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

        private bool NotAPrime = false;
    }
}

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

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

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'interfaccia utente è più importante della ricerca di numeri. Viene inoltre passato un nuovo delegato per la rappresentazione della routine di ricerca dei numeri.

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

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

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

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

private bool NotAPrime = false;

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'interfaccia utente. 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.

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'interfaccia utente perché non è possibile modificare direttamente la GUI dal thread di lavoro. Utilizzare Invoke o BeginInvoke per inserire delegati nell'oggetto Dispatcher del thread dell'interfaccia utente. Questi delegati verranno eseguiti con l'autorizzazione a modificare gli elementi dell'interfaccia utente.

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'interfaccia utente.

Schermata dell'interfaccia sulle previsioni del tempo

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 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. Disattivare 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 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'interfaccia utente 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.

  • Aggiornamento dell'interfaccia utente

    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'interfaccia utente esegue la 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".

Per il codice sorgente completo di questo esempio, vedere Esempio di simulazione di servizio meteorologico tramite Dispatcher.

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.

Windows Explorer funziona in questo modo. Ogni nuova finestra di Explorer 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 Explorer. 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>
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 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 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.

Per il codice sorgente completo di questo esempio, vedere Esempio di browser Web multithreading.

Dettagli tecnici e difficoltà

Scrittura di componenti utilizzando il threading

Nella Guida per gli sviluppatori di Microsoft .NET Framework viene descritto un pattern 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 pattern di Microsoft .NET Framework standard, il componente dovrebbe avere il seguente aspetto.

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 Form 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'interfaccia utente.

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 nidificata

Talvolta non è fattibile bloccare completamente il thread dell'interfaccia utente. 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. 

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 disattivati nella finestra originale (fare riferimento alla sezione relativa all'esclusione reciproca). In WPF viene invece utilizzato un sistema di elaborazione dei messaggi nidificato. 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 nidificati, 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 nidificato 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 nidificati 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 conoscenza, 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'interfaccia utente 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 interfaccia utente. Ed è qui che sorge il problema, perché un oggetto STA COM creato sul thread dell'interfaccia utente può essere eliminato solo sul thread dell'interfaccia utente. CLR esegue funzioni equivalenti a un oggetto BeginInvoke (in questo caso l'utilizzo di SendMessage Win32). Tuttavia, se il thread dell'interfaccia utente è 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

Attività

Esempio di applicazione a thread singolo con calcolo di lunga durata

Esempio di simulazione di servizio meteorologico tramite Dispatcher

Esempio di browser Web multithreading