Compartir a través de


Modelo de hilos

Windows Presentation Foundation (WPF) está diseñado para salvar a los desarrolladores de las dificultades de los hilos. Como resultado, la mayoría de los desarrolladores de WPF no escriben una interfaz que use más de un subproceso. Dado que los programas multiproceso son complejos y difíciles de depurar, deben evitarse cuando existen soluciones de un solo subproceso.

Por más bien diseñado que esté, ningún marco de interfaz de usuario puede ofrecer una solución monohilo para cada tipo de problema. WPF se acerca, pero todavía hay situaciones en las que varios subprocesos mejoran la capacidad de respuesta de la interfaz de usuario (UI) o el rendimiento de la aplicación. Después de analizar algún material de fondo, en este artículo se exploran algunas de estas situaciones y, a continuación, se concluye con una explicación de algunos detalles de nivel inferior.

Nota:

En este tema se aborda el tema del subproceso utilizando el método InvokeAsync para llamadas asincrónicas. El método InvokeAsync toma un Action o Func<TResult> como parámetro y devuelve un DispatcherOperation o DispatcherOperation<TResult>, que tiene la propiedad Task. Puede usar la palabra clave await con DispatcherOperation o con el asociado Task. Si necesita esperar sincrónicamente el Task que es devuelto por un DispatcherOperation o DispatcherOperation<TResult>, llame al método de extensión DispatcherOperationWait. La llamada Task.Wait provocará un interbloqueo. Para obtener más información sobre cómo usar Task para realizar operaciones asincrónicas, consulte Programación asincrónica basada en tareas.

Para realizar una llamada sincrónica, use el método Invoke, que también tiene sobrecargas que aceptan un delegado, Action, o un parámetro Func<TResult>.

Información general y el distribuidor

Normalmente, las aplicaciones WPF comienzan con dos subprocesos: uno para controlar la representación y otro para administrar la interfaz de usuario. El subproceso de representación se ejecuta eficazmente oculto en segundo plano mientras el subproceso de la interfaz de usuario recibe entradas, controla eventos, pinta la pantalla y ejecuta código de aplicación. La mayoría de las aplicaciones usan un único subproceso de interfaz de usuario, aunque en algunas situaciones es mejor usar varios. Analizaremos esto con un ejemplo más adelante.

El subproceso de interfaz de usuario pone en cola elementos de trabajo dentro de un objeto denominado Dispatcher. Dispatcher Selecciona los elementos de trabajo de forma prioritaria y ejecuta cada uno para su finalización. Cada subproceso de interfaz de usuario debe tener al menos un Dispatchery cada uno Dispatcher puede ejecutar elementos de trabajo en exactamente un subproceso.

El truco para crear aplicaciones dinámicas y fáciles de usar es maximizar el Dispatcher rendimiento manteniendo pequeños los elementos de trabajo. De este modo, los elementos nunca pierden frescura mientras están en la Dispatcher cola esperando a ser procesados. Cualquier retraso percebible entre la entrada y la respuesta puede frustrar a un usuario.

¿Cómo se supone que las aplicaciones WPF controlan las operaciones grandes? ¿Qué ocurre si el código implica un cálculo grande o necesita consultar una base de datos en algún servidor remoto? Normalmente, la respuesta es manejar la operación grande en un subproceso independiente, dejando libre el hilo de interfaz de usuario para atender los elementos de la Dispatcher cola. Una vez completada la operación grande, puede notificar su resultado al subproceso de la interfaz de usuario para su visualización.

Históricamente, Windows permite el acceso a los elementos de la interfaz de usuario solo mediante el subproceso que los creó. Esto significa que un subproceso en segundo plano encargado de alguna tarea de larga duración no puede actualizar un cuadro de texto cuando haya finalizado. Windows lo hace para garantizar la integridad de los componentes de la interfaz de usuario. Un cuadro de lista podría parecer extraño si su contenido se actualizara mediante un hilo de fondo durante el repintado.

WPF tiene un mecanismo integrado de exclusión mutua que exige esta coordinación. La mayoría de las clases de WPF derivan de DispatcherObject. Al momento de la construcción, un DispatcherObject almacena una referencia al Dispatcher vinculado al hilo que se está ejecutando actualmente. El DispatcherObject se asocia con el subproceso que lo crea. Durante la ejecución del programa, un DispatcherObject puede llamar a su método público VerifyAccess . VerifyAccess examina el Dispatcher asociado al hilo actual y lo compara con la referencia almacenada durante su construcción. Si no coinciden, VerifyAccess produce una excepción. VerifyAccess está pensado para ser invocado al inicio de cada método perteneciente a un DispatcherObject.

Si solo un subproceso puede modificar la interfaz de usuario, ¿cómo interactúan los subprocesos en segundo plano con el usuario? Un subproceso en segundo plano puede pedir al subproceso de interfaz de usuario que realice una operación en su nombre. Para ello, registra un elemento de trabajo con el Dispatcher del subproceso de interfaz de usuario. La Dispatcher clase proporciona los métodos para registrar elementos de trabajo: Dispatcher.InvokeAsync, Dispatcher.BeginInvokey Dispatcher.Invoke. Estos métodos programan un delegado para su ejecución. Invoke es una llamada sincrónica: es decir, no devuelve hasta que el subproceso de la interfaz de usuario termina realmente de ejecutar el delegado. InvokeAsync y BeginInvoke son asincrónicos y devuelven inmediatamente.

El Dispatcher ordena los elementos de su cola por prioridad. Hay diez niveles que se pueden especificar al agregar un elemento a la Dispatcher cola. Estas prioridades se mantienen en la DispatcherPriority enumeración.

Aplicación de un solo subproceso con un cálculo de ejecución prolongada

La mayoría de las interfaces gráficas de usuario (GUIs) pasan una gran parte de su tiempo de inactividad mientras esperan eventos que se generan en respuesta a las interacciones del usuario. Con una programación cuidadosa, este tiempo de inactividad se puede usar de forma constructiva, sin afectar 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 se produce en el subproceso de interfaz de usuario. Esto significa que debe asegurarse de volver al Dispatcher periódicamente para procesar eventos de entrada pendientes antes de que caduquen.

Se puede descargar una aplicación de ejemplo que muestre los conceptos de esta sección desde GitHub para C# o Visual Basic.

Considere el ejemplo siguiente:

Captura de pantalla que muestra la secuenciación de números primos.

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

Aunque es lo suficientemente simple, la búsqueda de números primos podría continuar para siempre, lo que presenta algunas dificultades. Si manejáramos toda la búsqueda dentro del controlador de eventos de clic del botón, nunca le daríamos a la interfaz de usuario una oportunidad para gestionar otros eventos. La interfaz de usuario no podría responder a los mensajes de entrada o proceso. Nunca volvería a pintar y nunca respondería a los clics de botón.

Podríamos realizar la búsqueda de números primos en un subproceso independiente, pero después tendríamos que tratar los problemas de sincronización. Con un enfoque de un único hilo, podemos actualizar directamente la etiqueta que indica el primo más grande encontrado.

Si desglosamos la tarea de cálculo en fragmentos administrables, podemos volver periódicamente a los Dispatcher eventos y procesarlos. Podemos dar a WPF una oportunidad para volver a pintar y procesar la entrada.

La mejor manera de dividir el tiempo de procesamiento entre el cálculo y el control de eventos es administrar el cálculo desde .Dispatcher Mediante el InvokeAsync método , podemos programar comprobaciones de números primos en la misma cola de la que se extraen los eventos de interfaz de usuario. En nuestro ejemplo, programamos solo una comprobación de número primo a la vez. Una vez completada la comprobación de números primos, programamos la siguiente comprobación inmediatamente. Esta comprobación continúa solo después de que se hayan controlado los eventos de interfaz de usuario pendientes.

Captura de pantalla que muestra la cola del distribuidor.

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

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

Importante

El CÓDIGO XAML que se muestra en este artículo procede de un proyecto de C#. XAML de Visual Basic es ligeramente diferente al declarar la clase asociada para el XAML.

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>

En el ejemplo siguiente se muestra el código subyacente.

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

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

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

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}
Imports System.Windows.Threading

Public Class PrimeNumber
    ' Current number to check
    Private _num As Long = 3
    Private _runCalculation As Boolean = False

    Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
        _runCalculation = Not _runCalculation

        If _runCalculation Then
            StartStopButton.Content = "Stop"
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        Else
            StartStopButton.Content = "Resume"
        End If

    End Sub

    Public Sub CheckNextNumber()
        ' Reset flag.
        _isPrime = True

        For i As Long = 3 To Math.Sqrt(_num)
            If (_num Mod i = 0) Then

                ' Set Not a prime flag to true.
                _isPrime = False
                Exit For
            End If
        Next

        ' If a prime number, update the UI text
        If _isPrime Then
            bigPrime.Text = _num.ToString()
        End If

        _num += 2

        ' Requeue this method on the dispatcher
        If (_runCalculation) Then
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        End If
    End Sub

    Private _isPrime As Boolean
End Class

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

Como se mencionó anteriormente, InvokeAsync es el Dispatcher miembro que se usa para programar un delegado para su ejecución. En este caso, la prioridad SystemIdle es la que elegimos. Dispatcher Ejecutará este delegado solo cuando no haya eventos importantes que 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 comprobación de números.

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

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

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    _isPrime = True

    For i As Long = 3 To Math.Sqrt(_num)
        If (_num Mod i = 0) Then

            ' Set Not a prime flag to true.
            _isPrime = False
            Exit For
        End If
    Next

    ' If a prime number, update the UI text
    If _isPrime Then
        bigPrime.Text = _num.ToString()
    End If

    _num += 2

    ' Requeue this method on the dispatcher
    If (_runCalculation) Then
        StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
    End If
End Sub

Private _isPrime As Boolean

Este método comprueba si el siguiente número impar es primo. Si es primo, el método actualiza bigPrimeTextBlock directamente para reflejar su detección. Podemos hacerlo porque el cálculo se está produciendo en el mismo subproceso que se usó para crear el control. Si decidimos usar un subproceso independiente para el cálculo, tendríamos que usar un mecanismo de sincronización más complicado y ejecutar la actualización en el subproceso de la interfaz de usuario. Demostraremos esta situación a continuación.

Varias ventanas, varios subprocesos

Algunas aplicaciones de WPF requieren varias ventanas de nivel superior. Es perfectamente aceptable que una combinación de hilo/despachador gestione varias ventanas, pero a veces varios hilos hacen un mejor trabajo. Esto es especialmente cierto si hay alguna posibilidad de que una de las ventanas monopolice el hilo.

El Explorador de Windows funciona de esta manera. Cada nueva ventana del Explorador pertenece al proceso original, pero se crea bajo el control de un subproceso independiente. Cuando el Explorador deja de responder, como cuando se buscan recursos de red, otras ventanas del Explorador siguen respondiendo y se pueden usar.

Podemos demostrar este concepto con el ejemplo siguiente.

Captura de pantalla de una ventana de WPF duplicada cuatro veces. Tres de las ventanas indican que usan el mismo subproceso, mientras que los otros dos están en subprocesos diferentes.

Las tres primeras ventanas de esta imagen comparten el mismo identificador de subproceso: 1. Las otras dos ventanas tienen diferentes identificadores de subprocesos: 9 y 4. Hay un glifo giratorio de color magenta ‼️ en la parte superior derecha de cada ventana.

Este ejemplo contiene una ventana con un glifo giratorio ‼️ , un botón Pausar y otros dos botones que crean una nueva ventana bajo el subproceso actual o en un subproceso nuevo. El ‼️ glifo gira constantemente hasta que se presiona el botón Pausar , que pausa el subproceso durante cinco segundos. En la parte inferior de la ventana, se muestra el identificador del hilo.

Cuando se presiona el botón Pausar , todas las ventanas debajo del mismo subproceso dejan de responder. Cualquier ventana de un subproceso diferente sigue funcionando normalmente.

El siguiente ejemplo es el XAML para la ventana:

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>

En el ejemplo siguiente se muestra el código subyacente.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

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

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}
Imports System.Threading

Public Class MultiWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
    End Sub

    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub

    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub

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

    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()

        System.Windows.Threading.Dispatcher.Run()
    End Sub
End Class

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

  • La Task.Delay(TimeSpan) tarea se usa para hacer que el subproceso actual se detenga durante cinco segundos cuando se presiona el botón Pausar .

    private void PauseButton_Click(object sender, RoutedEventArgs e) =>
        Task.Delay(TimeSpan.FromSeconds(5)).Wait();
    
    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub
    
  • El SameThreadWindow_Click controlador de eventos muestra inmedientemente una nueva ventana bajo el subproceso actual. El NewThreadWindow_Click controlador de eventos crea un nuevo subproceso que comienza a ejecutar el método ThreadStartingPoint, que a su vez muestra una nueva ventana, como se describe en el siguiente punto de viñeta.

    private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
        new MultiWindow().Show();
    
    private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
    {
        Thread newWindowThread = new Thread(ThreadStartingPoint);
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    }
    
    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub
    
    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    
  • El ThreadStartingPoint método es el punto de partida del nuevo subproceso. La nueva ventana se crea bajo el control de este hilo. WPF crea automáticamente un nuevo System.Windows.Threading.Dispatcher para administrar el nuevo subproceso. Todo lo que tenemos que hacer para que la ventana sea funcional es iniciar el System.Windows.Threading.Dispatcher.

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

Se puede descargar una aplicación de ejemplo que muestre los conceptos de esta sección desde GitHub para C# o Visual Basic.

Control de una operación de bloqueo con Task.Run

Controlar las operaciones de bloqueo en una aplicación gráfica puede ser difícil. No queremos llamar a métodos de bloqueo desde controladores de eventos porque la aplicación parece congelarse. En el ejemplo anterior se crearon nuevas ventanas en su propio subproceso, lo que permite que cada ventana se ejecute de forma independiente entre sí. Aunque podemos crear un nuevo subproceso con System.Windows.Threading.Dispatcher, resulta difícil sincronizar el nuevo subproceso con el subproceso principal de la interfaz de usuario una vez completado el trabajo. Dado que el nuevo subproceso no puede modificar la interfaz de usuario directamente, tenemos que usar Dispatcher.InvokeAsync, Dispatcher.BeginInvokeo Dispatcher.Invoke, para insertar delegados en el Dispatcher del subproceso de la interfaz de usuario. Eventualmente, estos delegados se ejecutan con permiso para modificar los elementos de la interfaz de usuario.

Hay una manera más fácil de ejecutar el código en un nuevo subproceso al sincronizar los resultados, el patrón asincrónico basado en tareas (TAP). Se basa en los tipos Task y Task<TResult> del espacio de nombres System.Threading.Tasks que se usan para representar operaciones asincrónicas. TAP usa un único método para representar el inicio y la finalización de una operación asincrónica. Hay algunas ventajas para este patrón:

  • El autor de la llamada de puede Task elegir ejecutar el código de forma asincrónica o sincrónica.
  • El progreso se puede notificar desde el Task.
  • El código de llamada puede suspender la ejecución y esperar el resultado de la operación.

Ejemplo de Task.Run

En este ejemplo, imitamos una llamada a procedimiento remoto que recupera una previsión meteorológica. Cuando se hace clic en el botón, la interfaz de usuario se actualiza para indicar que la captura de datos está en curso, mientras se inicia una tarea para imitar la captura de la previsión meteorológica. Cuando se inicia la tarea, el código del controlador de eventos del botón se suspende hasta que finaliza la tarea. Una vez finalizada la tarea, el código del controlador de eventos continúa ejecutándose. El código está suspendido y no bloquea el resto del subproceso de la interfaz de usuario. El contexto de sincronización de WPF controla la suspensión del código, lo que permite que WPF continúe ejecutándose.

Diagrama que muestra el flujo de trabajo de la aplicación de ejemplo.

Diagrama que muestra el flujo de trabajo de la aplicación de ejemplo. La aplicación tiene un solo botón con el texto "Fetch Forecast". Hay una flecha que apunta a la siguiente fase de la aplicación después de presionar el botón, que es una imagen de reloj situada en el centro de la aplicación que indica que la aplicación está ocupada capturando datos. Después de algún tiempo, la aplicación vuelve con una imagen del sol o nubes de lluvia, según el resultado de los datos.

Se puede descargar una aplicación de ejemplo que muestre los conceptos de esta sección desde GitHub para C# o Visual Basic. El CÓDIGO XAML de este ejemplo es bastante grande y no se proporciona en este artículo. Use los vínculos anteriores de GitHub para examinar el CÓDIGO XAML. El XAML usa un solo botón para obtener el clima.

Tenga en cuenta el código subyacente en XAML.

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

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

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

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}
Imports System.Windows.Media.Animation

Public Class Weather

    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)

        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)

        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)

        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)

        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)

        End If

        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)

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

    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)

        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))

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

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

    End Function

    Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
    End Sub

    Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
    End Sub
End Class

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

  • Controlador de eventos de botón

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Observe que el controlador de eventos se declaró con async (o Async con Visual Basic). Un método "asincrónico" permite la suspensión del código cuando se llama a un método esperado, como FetchWeatherFromServerAsync, . Esto se designa mediante la await palabra clave (o Await con Visual Basic). FetchWeatherFromServerAsync Hasta que finalice, el código del controlador del botón se suspende y el control se devuelve al autor de la llamada. Esto es similar a un método sincrónico, excepto que un método sincrónico espera a que todas las operaciones del método finalicen después de la cual se devuelve el control al autor de la llamada.

    Los métodos esperados utilizan el contexto de subproceso del método actual, que con el controlador de botones, es el subproceso de la interfaz de usuario. Esto significa que llamar a await FetchWeatherFromServerAsync(); (o a Await FetchWeatherFromServerAsync() con Visual Basic) hace que el código en FetchWeatherFromServerAsync se ejecute en el subproceso de la interfaz de usuario, pero no se ejecuta hasta que el dispatcher tiene tiempo para hacerlo, de forma similar a cómo opera el ejemplo de una aplicación de un solo subproceso con un cálculo de larga duración. Sin embargo, observe que await Task.Run se usa. Esto crea un nuevo subproceso en el grupo de subprocesos para la tarea designada en lugar del subproceso actual. Por lo tanto, FetchWeatherFromServerAsync se ejecuta en su propio subproceso.

  • Obteniendo el clima

    private async Task<string> FetchWeatherFromServerAsync()
    {
        // Simulate the delay from network access
        await Task.Delay(TimeSpan.FromSeconds(4));
    
        // Tried and true method for weather forecasting - random numbers
        Random rand = new Random();
    
        if (rand.Next(2) == 0)
            return "rainy";
        
        else
            return "sunny";
    }
    
    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
    
        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))
    
        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()
    
        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If
    
    End Function
    

    Para simplificar las cosas, realmente no tenemos ningún código de red en este ejemplo. En su lugar, simulamos el retraso del acceso a la red colocando el nuevo subproceso en suspensión durante cuatro segundos. En este momento, el subproceso de interfaz de usuario original sigue en ejecución y responde a eventos de interfaz de usuario mientras el controlador de eventos del botón se pausa hasta que se completa el nuevo subproceso. Para demostrar esto, hemos dejado una animación en ejecución y puedes cambiar el tamaño de la ventana. Si el subproceso de interfaz de usuario se ha pausado o retrasado, no se mostrará la animación y no se pudo interactuar con la ventana.

    Cuando Task.Delay esté terminado y hayamos seleccionado aleatoriamente nuestra previsión meteorológica, el estado meteorológico se devuelve al llamador.

  • Actualización de la interfaz de usuario

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Cuando la tarea finaliza y el subproceso de interfaz de usuario tiene tiempo, se reanuda el llamador, es decir, el manejador de eventos del botón. El resto del método detiene la animación del reloj y elige una imagen para describir el tiempo. Muestra esta imagen y habilita el botón "Obtener pronóstico".

Se puede descargar una aplicación de ejemplo que muestre los conceptos de esta sección desde GitHub para C# o Visual Basic.

Detalles técnicos y puntos problemáticos

En las siguientes secciones se describen algunos de los detalles y los puntos conflictivos que puede encontrar con la programación multihilo.

Bombeo anidado

A veces no es factible bloquear completamente el hilo de la interfaz de usuario. Consideremos el Show método de la MessageBox clase . Show no devuelve hasta que el usuario hace clic en el botón Aceptar. Sin embargo, crea una ventana que debe tener un bucle de mensajes para poder ser interactivo. Mientras esperamos que el usuario haga clic en Aceptar, la ventana de la aplicación original no responde a la entrada del usuario. Sin embargo, sigue procesando mensajes de pintura. La ventana original se redibuja al ser cubierta y revelada.

Captura de pantalla que muestra un cuadro de mensajes con un botón Aceptar

Algún subproceso debe encargarse de la ventana del cuadro de mensaje. WPF podría crear un nuevo hilo solo para la ventana del cuadro de diálogo del mensaje, pero este hilo no podría pintar los elementos deshabilitados en la ventana original (recuerde la explicación anterior sobre la exclusión mutua). En su lugar, WPF usa un sistema de procesamiento de mensajes anidado. La Dispatcher clase 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 PushFrame .

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

Eventos enrutados obsoletos

El sistema de eventos enrutado en WPF notifica a todos los árboles cuando se generan eventos.

<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 izquierdo del mouse sobre la elipse, handler2 se ejecuta. Una vez handler2 finalizado, el evento se pasa al Canvas objeto , que usa handler1 para procesarlo. Esto solo sucede si handler2 no marca explícitamente el objeto de evento como controlado.

Es posible que handler2 tarde mucho tiempo en procesar este evento. handler2 puede usar PushFrame para iniciar un bucle de mensajes anidado que no regresa durante horas. Si handler2 no marca el evento como gestionado cuando se complete este bucle de mensajes, el evento se pasa por el árbol aunque ya sea bastante antiguo.

Reentrada y bloqueo

El mecanismo de bloqueo de Common Language Runtime (CLR) no se comporta exactamente como uno podría imaginar; es posible que un subproceso deje 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 a hacer que las interfaces tengan una capacidad mínima de respuesta, pero presenta la posibilidad de errores sutiles. La gran mayoría del tiempo no necesitas saber nada sobre esto, pero en raras circunstancias (normalmente involucrando mensajes de ventana Win32 o componentes COM STA), puede ser valioso conocerlo.

La mayoría de las interfaces no se crean teniendo en cuenta la seguridad de subprocesos porque los desarrolladores trabajan bajo la suposición de que nunca más de un subproceso accede a una interfaz de usuario. En este caso, ese único subproceso puede realizar cambios en el entorno en momentos inesperados, causando esos efectos negativos que se supone que el mecanismo de exclusión mutua debería resolver. Tenga en cuenta el pseudocódigo siguiente:

Diagrama que muestra la reentrada de subprocesos.

La mayoría de las veces es lo correcto, pero hay ocasiones en WPF donde la reentrada inesperada puede realmente causar problemas. Por lo tanto, en determinados momentos clave, WPF llama a DisableProcessing, que cambia la instrucción de bloqueo para que ese subproceso use el bloqueo sin reentrada de WPF, en lugar del bloqueo CLR habitual.

¿Por qué decidió el equipo CLR elegir este comportamiento? Tenía que ver con los objetos COM STA y el subproceso de finalización. Cuando se recoge un objeto como basura, su método Finalize se ejecuta en el hilo dedicado del finalizador, no en el hilo de la interfaz de usuario. Y ahí se encuentra el problema, ya que un objeto COM STA que se creó en el subproceso de interfaz de usuario solo se puede eliminar en el subproceso de la interfaz de usuario. El CLR hace el equivalente de un BeginInvoke (en este caso, mediante Win32 SendMessage). Pero si el subproceso de la interfaz de usuario está ocupado, el subproceso del finalizador se detiene y el objeto COM STA no se puede eliminar, lo que crea una pérdida de memoria seria. Por lo tanto, el equipo de CLR tomó la difícil decisión de que los bloqueos funcionaran de la manera en que lo hacen.

La tarea de WPF es evitar la reentrada inesperada sin volver a introducir la pérdida de memoria, por lo que no se bloquea la reentrada en todas partes.

Consulte también