Compartir a través de


Modelo de subprocesos

Actualización: noviembre 2007

Windows Presentation Foundation (WPF) se ha diseñado para ahorrar a los programadores las dificultades de los subprocesos. Como resultado, la mayoría de los programadores de WPF no tendrán que escribir una interfaz que utilice más de un subproceso. Dado que los programas con subprocesos son complejos y difíciles de depurar, se deben evitar cuando existan soluciones de un único subproceso.

No importa cómo esté de bien estructurado, no obstante, ningún marco de trabajo de interfaz de usuario podrá ofrecer siempre una solución de un único subproceso para todo tipo de problemas. WPF se acerca, pero hay todavía situaciones en las que varios subprocesos mejorarán la capacidad de respuesta de la interfaz de usuario (UI) o el rendimiento de la aplicación. Después de explicar algunos temas básicos, este documento explora algunas de estas situaciones y, a continuación, concluye con una explicación de algunos detalles de más bajo nivel.

Este tema contiene las secciones siguientes.

  • Información general y el distribuidor
  • Subprocesos en acción: ejemplos
  • Detalles técnicos y puntos problemáticos
  • Temas relacionados

Información general y el distribuidor

Habitualmente, las aplicaciones de WPF se inician con dos subprocesos: uno para administrar la representación y otro para administrar la interfaz de usuario. El subproceso de representación se ejecuta realmente de forma oculta en segundo plano, mientras que el subproceso de la interfaz de usuario recibe la entrada, administra los eventos, pinta la pantalla y ejecuta el código de aplicación. La mayoría de las aplicaciones utilizan un único subproceso de interfaz de usuario, aunque en algunas situaciones es mejor utilizar varios. Lo explicaremos después con un ejemplo.

El subproceso de interfaz de usuario pone en la cola los elementos de trabajo dentro de un objeto denominado DispatcherDispatcher selecciona los elementos de trabajo en función de la prioridad y ejecuta cada uno hasta que se completan. Cada subproceso de la interfaz de usuario debe tener al menos un objeto Dispatcher, y cada Dispatcher puede ejecutar elementos de trabajo en exactamente un subproceso.

El truco para generar aplicaciones eficaces y fáciles de usar es maximizar el rendimiento de Dispatcher manteniendo pequeño el tamaño de los elementos de trabajo. De este modo, los elementos nunca quedan obsoletos en la cola Dispatcher a la espera de ser procesados. Cualquier retraso perceptible entre la entrada y la respuesta puede frustrar a un usuario.

¿Cómo se supone, entonces, que las aplicaciones WPF administran grandes operaciones? ¿Qué ocurre si el código implica un cálculo grande o necesita consultar una base de datos de algún servidor remoto? Normalmente, la respuesta es administrar la operación grande en un subproceso independiente, dejando el subproceso de la interfaz de usuario libre para ocuparse de los elementos de la cola de Dispatcher. Cuando la operación grande se haya completado, puede informar de su resultado al subproceso de la interfaz de usuario para la presentación.

Históricamente, Windows solamente permite obtener acceso a los elementos de la interfaz de usuario al subproceso que los creó. Esto significa que un subproceso de fondo a cargo de alguna tarea de ejecución prolongada no podrá actualizar un cuadro de texto cuando se haya completado. Windows lo hace así para garantizar la integridad de los componentes de la interfaz de usuario. Un cuadro de lista puede parecer extraño si un subproceso de fondo actualiza su contenido durante la presentación.

WPF tiene un mecanismo de exclusión mutua integrado que exige esta coordinación. La mayoría de las clases de WPF se derivan de DispatcherObject. En la construcción, un objeto DispatcherObject almacena una referencia al objeto Dispatcher vinculado al subproceso actualmente en ejecución. En efecto, DispatcherObject se asocia al subproceso que lo crea. Durante la ejecución de programas, un objeto DispatcherObject puede llamar a su método público VerifyAccess. VerifyAccess examina el objeto Dispatcher asociado al subproceso actual y lo compara con la referencia Dispatcher almacenada durante la construcción. Si no coinciden, VerifyAccess inicia una excepción. VerifyAccess está destinado a ser llamado al principio de cada método perteneciente a un objeto DispatcherObject.

¿Si solamente un subproceso puede modificar la interfaz de usuario, cómo interactúan con el usuario los subprocesos de fondo? Un subproceso de fondo puede pedir al subproceso de la interfaz de usuario que realice una operación en su nombre. Lo hace registrando un elemento de trabajo con el objeto Dispatcher del subproceso de la interfaz de usuario. La clase Dispatcher proporciona dos métodos para registrar elementos de trabajo: Invoke y BeginInvoke. Ambos métodos programan un delegado para la ejecución. Invoke es una llamada sincrónica, es decir, no vuelve hasta que el subproceso de la interfaz de usuario termina realmente de ejecutar el delegado. BeginInvoke es asincrónico y vuelve inmediatamente.

El objeto Dispatcher ordena los elementos de la cola por prioridad. Hay diez niveles que se puede especificar al agregar un elemento a la cola Dispatcher. Estas prioridades se mantienen en la enumeración DispatcherPriority. Puede encontrar información detallada sobre los niveles de DispatcherPriority en la documentación de Windows SDK.

Subprocesos en acción: ejemplos

Una aplicación de un único subproceso con un cálculo de ejecución prolongada

La mayoría de las interfaces gráficas de usuario (GUIs) emplean una gran parte de su tiempo inactivo esperando eventos generados en respuesta a las interacciones con el usuario Con una programación cuidadosa, este tiempo de inactividad se puede usar constructivamente, sin que afecte a la capacidad de respuesta de la interfaz de usuario. El modelo de subprocesos de WPF no permite que la entrada interrumpa una operación que ocurra en el subproceso de la interfaz de usuario. Esto significa que debe asegurarse de volver periódicamente al objeto Dispatcher para procesar los eventos de entrada pendientes antes de que queden obsoletos.

Considere el ejemplo siguiente:

Captura de pantalla de números primos

Esta sencilla aplicación cuenta en orden ascendente desde tres, buscando números primos. Cuando el usuario hace clic en el botón Inicio, la búsqueda comienza. Cuando el programa encuentra un número primo, actualiza la interfaz de usuario con su detección. En cualquier punto, el usuario puede detener la búsqueda.

Aunque es bastante simple, la búsqueda de números primos podría continuar para siempre, lo que presenta algunas dificultades. Si administráramos toda la búsqueda completa dentro del controlador del evento clic del botón, nunca daríamos al subproceso de la interfaz de usuario la oportunidad de administrar otros eventos. La interfaz de usuario no podría responder a entradas ni mensajes del proceso. Nunca actualizaría la pantalla ni respondería a clics del mouse.

Podríamos realizar la búsqueda de números primos en un subproceso independiente, pero entonces encontraríamos problemas de sincronización. Con un enfoque de un único subproceso, podemos actualizar directamente la etiqueta que muestra el mayor número encontrado.

Si dividimos la tarea de cálculo en fragmentos manejables, podemos volver periódicamente a los eventos de Dispatcher y de proceso Podemos dar una oportunidad a WPF para que vuelva a dibujar y procesar la entrada.

La mejor forma de dividir el tiempo de proceso entre el cálculo y el control de eventos es administrar el cálculo desde el objeto Dispatcher. Utilizando el método BeginInvoke, podemos programar las comprobaciones de números primos en la misma cola de la que se extraen los eventos de la interfaz de usuario. En nuestro ejemplo, programamos solamente una comprobación de número primo cada vez. Una vez completada la comprobación del primer número primo, programamos inmediatamente la siguiente comprobación. Esta comprobación sólo continúa una vez administrados los eventos de la interfaz de usuario pendientes.

Ilustración de cola del distribuidor

Microsoft Word realiza la revisión ortográfica mediante este mecanismo. La revisión ortográfica se hace en segundo plano utilizando el tiempo de inactividad del subproceso de la interfaz de usuario. Echemos una mirada al código.

En el ejemplo siguiente se muestra el XAML que crea la interfaz de usuario.

<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>

En el código siguiente se muestra el código subyacente:

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;
    }
}

En el ejemplo siguiente se muestra el controlador de evento para el control 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));
    }
}

Además de actualizar el texto del control Button, este controlador es responsable de programar la primera comprobación de número primo agregando un delegado a la cola de Dispatcher. En algún momento después de que este controlador de eventos haya completado su trabajo, Dispatcher seleccionará este delegado para la ejecución.

Como mencionamos anteriormente, BeginInvoke es el miembro Dispatcher que se utiliza para programar un delegado para la ejecución. En este caso, elegimos la prioridad SystemIdle. El objeto Dispatcher solamente ejecutará este delegado cuando no haya ningún evento importante por procesar. La capacidad de respuesta de la interfaz de usuario es más importante que la comprobación de números. También pasamos un nuevo delegado que representa la rutina de la comprobación de números.

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;

Este método comprueba si el siguiente número impar es primo. Si es primo, el método actualiza directamente el controlbigPrimeTextBlock para que refleje su detección. Podemos hacerlo porque el cálculo se está produciendo en el mismo subproceso que se utilizó para crear el componente. Si hubiéramos decidido utilizar un subproceso independiente para el cálculo, tendríamos que haber utilizado un mecanismo de sincronización más complicado y ejecutar la actualización en el subproceso de la interfaz de usuario. Mostraremos esta situación a continuación.

Para obtener el código fuente completo de este ejemplo, vea Ejemplo Single-Threaded Application with Long-Running Calculation.

Administrar una operación de bloqueo con un subproceso de fondo

Administrar operaciones de bloqueo en una aplicación gráfica puede ser difícil. No es deseable llamar a métodos de bloqueo desde controladores de eventos, porque la aplicación parecerá congelarse. Podemos utilizar un subproceso independiente para administrar estas operaciones, pero cuando terminemos, tendremos que sincronizar con el subproceso de la interfaz de usuario porque no podemos modificar directamente la GUI desde nuestro subproceso de trabajo. Podemos utilizar Invoke o BeginInvoke para insertar delegados en el objeto Dispatcher del subproceso de la interfaz de usuario. En el futuro, estos delegados se ejecutarán con permiso para modificar los elementos de la interfaz de usuario.

En este ejemplo, imitamos una llamada a procedimiento remoto que recupera un boletín meteorológico. Utilizamos un subproceso de trabajo independiente para ejecutar esta llamada y programamos un método de actualización en el objeto Dispatcher del subproceso interfaz de usuario cuando terminamos.

Captura de pantalla de IU de información meteorológica

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);
        }        
    }
}

A continuación se muestran algunos de los detalles que se deben tener en cuenta.

  • Crear el controlador del botón

    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);
    }
    

Cuando se hace clic en el botón, se muestra el dibujo del reloj y se inicia su animación. Deshabilitamos el botón. Invocamos el método FetchWeatherFromServer en un nuevo subproceso y a continuación volvemos, para permitir que el objeto Dispatcher procese los eventos mientras esperamos la llegada del boletín meteorológico.

  • Capturar la información meteorológica

    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);
    }
    

Para mantener las cosas simples, en este ejemplo no tenemos realmente ningún código de conexión de red. En su lugar, simulamos el retraso de acceso de red haciendo que nuestro nuevo subproceso espere durante cuatro segundos. En este tiempo, el subproceso de la interfaz de usuario original continúa ejecutándose y respondiendo a eventos. Para mostrarlo, hemos dejado la animación ejecutándose y los botones maximizar y minimizar también continúan funcionando.

  • Actualizar la interfaz de usuario

    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;     
    }
    

Cuando el objeto Dispatcher del subproceso de la interfaz de usuario tiene tiempo, ejecuta la llamada programada a UpdateUserInterface. Este método detiene la animación del reloj y elige una imagen que describa la información meteorológica. Muestra esta imagen y restaura el botón "fetch forecast" (capturar previsión).

Para obtener el código fuente completo de este ejemplo, vea Ejemplo Weather Service Simulation via Dispatcher.

Varias ventanas, varios subprocesos

Algunas aplicaciones de WPF requieren varias ventanas de nivel superior. Es absolutamente aceptable que una combinación de subproceso/Dispatcher administre varias ventanas, pero hay ocasiones en las que varios subprocesos funcionarán mejor. Esto es especialmente cierto si existe alguna oportunidad de que una de las ventanas monopolice el subproceso.

El Explorador de Windows funciona de este modo. Cada nueva ventana del Explorador pertenece al proceso original, pero se crea bajo el control de un subproceso independiente.

Utilizando un control Frame de WPF, podemos mostrar páginas web. Podemos crear fácilmente un sustituto sencillo de Internet Explorer. Empezamos con una característica importante: la capacidad de abrir una nueva ventana del explorador. Cuando el usuario hace clic en el botón "new window" (nueva ventana), iniciamos una copia de nuestra ventana en un subproceso independiente. De este modo, las operaciones de ejecución prolongada o de bloqueo de una de las ventanas no bloquearán todas las otras ventanas.

En realidad, el modelo del explorador web tiene su propio y complicado modelo de subprocesos. Lo hemos elegido porque debe resultar conocido para la mayoría de los lectores.

En el ejemplo siguiente se muestra el código.

<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();
        }
    }
}

Los siguientes segmentos de subprocesamiento de este código son los más interesantes para nosotros en este contexto:

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

Se llama a este método cuando se hace clic en el botón "new window". Crea un nuevo subproceso y lo inicia de forma asincrónica.

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

Este método es el punto inicial para el nuevo subproceso. Creamos una nueva ventana bajo el control de este subproceso. WPF crea automáticamente un nuevo objeto Dispatcher para administrar el nuevo subproceso. Todo lo que tenemos que hacer para que la ventana sea funcional es iniciar el objeto Dispatcher.

Para obtener el código fuente completo de este ejemplo, vea Ejemplo Multithreading Web Browser.

Detalles técnicos y puntos problemáticos

Escribir componentes usando subprocesos

La Guía del desarrollador de Microsoft .NET Framework describe un modelo de cómo un componente puede exponer comportamiento asincrónico a sus clientes (vea Información general sobre el modelo asincrónico basado en eventos). Por ejemplo, suponga que deseamos empaquetar el método FetchWeatherFromServer en un componente reutilizable, no gráfico. Siguiendo el modelo de Microsoft .NET Framework estándar, tendría un aspecto similar al siguiente.

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 utilizaría una de las técnicas antes descritas, tal como crear un subproceso de fondo para hacer el trabajo de forma asincrónica, sin bloquear el subproceso de llamada.

Una de las partes más importantes de este modelo es la llamada al método MethodNameCompleted en el mismo subproceso que llamó al método MethodNameAsync para comenzar. Podría hacerlo con bastante facilidad con WPF, almacenando CurrentDispatcher; sin embargo, el componente no gráfico solamente se podría utilizar en aplicaciones de WPF, no en programas de formularios Windows Forms o ASP.NET. 

La clase DispatcherSynchronizationContext soluciona esta necesidad; piense en ella como en una versión simplificada de Dispatcher que también funciona con otros marcos de interfaz de usuario.

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();
}

Bombeo anidado

A veces no es factible bloquear completamente el subproceso de la interfaz de usuario. Consideremos el método Show de la clase MessageBox. Show no vuelve hasta que el usuario hace clic en el Aceptar. Sin embargo, crea una ventana que debe tener un bucle de mensajes para ser interactiva. Mientras estamos esperando a que el usuario haga clic en Aceptar, la ventana de la aplicación original no responde a la entrada del usuario. No obstante, continúa procesando los mensajes de actualización de la pantalla. La ventana original se redibuja cuando se cubre y se revela. 

MessageBox con un botón "Aceptar"

Algún subproceso debe estar a cargo de la ventana de cuadro de mensaje. WPF podría crear simplemente un nuevo subproceso para la ventana de cuadro de mensaje, pero este subproceso no podría representar los elementos deshabilitados en la ventana original (recuerde la explicación anterior sobre la exclusión mutua). En su lugar, WPF utiliza un sistema de procesado de mensajes anidados. La clase Dispatcher incluye un método especial denominado PushFrame, que almacena el punto de ejecución actual de una aplicación y, a continuación, comienza un nuevo bucle de mensajes. Cuando finaliza el bucle de mensajes anidados, la ejecución se reanuda después de la llamada original a PushFrame.

En este caso, PushFrame mantiene el contexto de programa en la llamada a MessageBox.Show e inicia un nuevo bucle de mensajes para actualizar la ventana de fondo y administrar la entrada en la ventana de cuadro de mensaje. Cuando el usuario hace clic en Aceptar y borra la ventana emergente, se sale del bucle anidado y se reanuda el control después de la llamada a Show.

Eventos enrutados obsoletos

El sistema de eventos enrutados de WPF notifica a los árboles completos cuando se provoca algún evento.

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

Cuando se presiona el botón primario del mouse sobre la elipse, se ejecuta handler2. Cuando finaliza handler2, el evento se pasa al objeto Canvas, que utiliza handler1 para procesarlo. Esto solamente pasa si handler2 no marca explícitamente el objeto de evento como administrado.

Es posible que handler2 tarde mucho tiempo en procesar este evento. handler2 podría utilizar PushFrame para iniciar un bucle de mensajes anidado que no volviera durante horas. Si handler2 no marca el evento como administrado cuando finaliza este bucle de mensajes, el evento se pasa al árbol aunque sea muy antiguo.

Volver a entrar y bloqueo

El mecanismo de bloqueo de common language runtime (CLR) no se comporta exactamente como se podría imaginar; podría esperarse que un subproceso cesara completamente de funcionar al solicitar un bloqueo. En realidad, el subproceso continúa recibiendo y procesando mensajes de alta prioridad. Esto ayuda a evitar interbloqueos y minimiza la sensibilidad de las interfaces, pero introduce la posibilidad de errores sutiles. Durante la mayor parte del tiempo no necesitará saber nada sobre esto pero, en raras circunstancias (que habitualmente implican mensajes de ventanas de Win32 o componentes COM STA) puede merecer la pena conocer este tema.

La mayoría de las interfaces no se han creado pensando en la seguridad de los subprocesos, porque los programadores trabajan partiendo del supuesto de que nunca habrá más de un subproceso con acceso a la interfaz de usuario. En este caso, ese subproceso único puede modificar el entorno en momentos inesperados, produciendo efectos indeseables que el mecanismo de exclusión mutua de DispatcherObject debe resolver. Considere el siguiente pseudocódigo:

Diagrama de reentrada de subprocesamiento

La mayor parte del tiempo es correcto, pero hay momentos en WPF en los que tal vuelta a entrar inesperada realmente puede causar problemas. Por lo tanto, en ciertos momentos clave, WPF llama a DisableProcessing, que cambia la instrucción de bloqueo para que ese subproceso utilice el bloqueo sin vuelta a entrar de WPF, en lugar del bloqueo CLRhabitual. 

Entonces, ¿por qué el equipo de CLR eligió este comportamiento? Tenía que ver con los objetos COM STA y el subproceso de finalización. Cuando un objeto se somete a la recopilación de elementos no utilizados, su método Finalize se ejecuta en el subproceso finalizador dedicado, no en el subproceso de la interfaz de usuario. Ahí reside el problema, porque un objeto COM STA creado en el subproceso de la interfaz de usuario sólo se puede desechar en el subproceso de la interfaz de usuario. CLR realiza el equivalente de BeginInvoke) (en este caso utilizando el método SendMessage de Win32. Sin embargo, si el subproceso de la interfaz de usuario está ocupado, el subproceso finalizador se atasca y no se puede desechar el objeto COM STA, lo que crea una grave pérdida de memoria. En consecuencia, el equipo de CLR tomó esta decisión para que los bloqueos funcionaran como lo hacen.  

La tarea de WPF es evitar que se vuelva a entrar de forma inesperada sin reintroducir la pérdida de memoria, que es el motivo por el que no bloqueamos la vuelta a entrar en todas partes.

Vea también

Tareas

Ejemplo Single-Threaded Application with Long-Running Calculation

Ejemplo Weather Service Simulation via Dispatcher

Ejemplo Multithreading Web Browser