Share via


Modelo de subprocesos

Windows Presentation Foundation (WPF) está diseñado para ahorrar a los desarrolladores las dificultades del subprocesamiento. Como resultado, la mayoría de los desarrolladores de WPF no tendrán que escribir una interfaz que utilice más de un subproceso. Dado que los programas con subprocesamiento múltiple 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 UI 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 user interface (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

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

El subproceso de UI 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 su finalización. Cada subproceso de UI debe tener al menos un 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 de WPF controlan las 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 controlar la operación grande en un subproceso independiente, dejando el subproceso de la UI 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 UI para la presentación.

Históricamente, Windows solamente permite obtener acceso a los elementos de la UI al subproceso que los creó. Esto significa que un subproceso en segundo plano 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 UI. Un cuadro de lista podría parecer extraño si un subproceso en segundo plano 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 derivan de DispatcherObject. En la construcción, un DispatcherObject almacena una referencia al Dispatcher vinculado al subproceso que se está ejecutando actualmente. En efecto, DispatcherObject se asocia al subproceso que lo crea. Durante la ejecución del programa, un DispatcherObject puede llamar a su método público VerifyAccessVerifyAccess examina el Dispatcher asociado al subproceso actual y lo compara con la referencia al Dispatcher almacenada durante la construcción. Si no coinciden, VerifyAccess produce una excepción. VerifyAccess está diseñado para que sea llamado al principio de cada método perteneciente a un DispatcherObject.

¿Si solamente un subproceso puede modificar la UI, cómo interactúan con el usuario los subprocesos en segundo plano? Un subproceso en segundo plano puede pedir al subproceso de la UI que realice una operación en su nombre. Lo hace registrando un elemento de trabajo con el Dispatcher del subproceso de la UI. 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 UI 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 graphical user interfaces (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 UI. El modelo de subprocesos de WPF no permite que la entrada interrumpa una operación que ocurra en el subproceso de la UI. Esto significa que debe asegurarse de volver periódicamente al 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 controláramos toda la búsqueda dentro del controlador de eventos de clic del botón, nunca daríamos al subproceso de la UI la oportunidad de controlar otros eventos. La UI no podría responder a entradas ni mensajes del proceso. Nunca se volvería a dibujar ni respondería a los clics del botón.

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. Mediante 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 UI. 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 solamente continúa después de controlar los eventos de la UI 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 UI. Echemos un vistazo 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>
<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>

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

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

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

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. Dispatcher solamente ejecutará este delegado cuando no haya ningún evento importante por procesar. La capacidad de respuesta de la UI es más importante que la comprobación de números. También pasamos un nuevo delegado que representa la rutina de comprobación de números.

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;

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 UI. Mostraremos esta situación a continuación.

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

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 controlar estas operaciones, pero cuando terminemos, tendremos que sincronizar con el subproceso de la UI porque no podemos modificar directamente la GUI desde nuestro subproceso de trabajo. Podemos utilizar Invoke o BeginInvoke para insertar delegados en el Dispatcher del subproceso de la UI. Finalmente, estos delegados se ejecutarán con permiso para modificar los elementos de la UI.

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 UI cuando terminamos.

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


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

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

  • Crear el controlador del botón

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

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

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

Cuando finaliza el retraso y hemos seleccionado aleatoriamente nuestro boletín meteorológico, es el momento de notificarlo al subproceso de la UI. Esto lo hacemos programando una llamada a UpdateUserInterface en el subproceso de la UI mediante el Dispatcher de dicho subproceso. Pasamos una cadena que describe el tiempo a esta llamada programada al método.

  • Actualizar la 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;     
    }
    

Cuando el Dispatcher del subproceso de la UI 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).

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.

Mediante un control WPF Frame, podemos mostrar páginas web. Podemos crear fácilmente un sustituto sencillo de Internet Explorer. Comenzamos 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>

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

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

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

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

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 Dispatcher para administrar el nuevo subproceso. Todo lo que tenemos que hacer para que la ventana sea funcional es iniciar el Dispatcher.

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

Bombeo anidado

A veces no es posible bloquear completamente el subproceso de la UI. Consideremos el método Show de la clase MessageBoxShow no vuelve hasta que el usuario hace clic en el botón 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 los datos proporcionados por el 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 un nuevo subproceso solamente 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 procesamiento 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 dejara de funcionar completamente 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 presenta 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 diseñado teniendo en cuenta la seguridad de los subprocesos, porque los desarrolladores trabajan partiendo del supuesto de que nunca habrá más de un subproceso con acceso a la UI. En este caso, ese subproceso único puede modificar el entorno en momentos inesperados, produciendo esos efectos no deseados que el mecanismo de exclusión mutua de DispatcherObject debe resolver. Considere el siguiente pseudocódigo:

Diagrama de reentrada de subprocesos

Casi siempre es lo correcto, pero hay ocasiones en WPF en las que esa reentrada inesperada realmente puede producir 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 reentrada de WPF, en lugar del bloqueo de CLR habitual. 

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 recolecta como elemento no utilizado, su método Finalize se ejecuta en el subproceso finalizador dedicado, no en el subproceso de la UI. Ahí reside el problema, porque un objeto COM STA creado en el subproceso de la UI solo se puede desechar en el subproceso de la UI. CLR realiza el equivalente de BeginInvoke (en este caso utilizando el método SendMessage de Win32). Sin embargo, si el subproceso de la UI 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

Otros recursos

Single-Threaded Application with Long-Running Calculation Sample