Modelo de subprocesos
Windows Presentation Foundation (WPF) está diseñado para salvar a los desarrolladores de las dificultades de subproceso. 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, se deben evitar cuando existan soluciones de un único subproceso.
Sin embargo, independientemente de lo bien diseñado, ningún marco de interfaz de usuario pueda proporcionar una solución de un solo subproceso 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 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 describe el subproceso usando el método InvokeAsync para las 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 una propiedad Task. Se puede usar la palabra clave await
con la operación DispatcherOperation o la tarea Task asociada. Si es necesario esperar de forma sincrónica la tarea Task devuelta por DispatcherOperation o DispatcherOperation<TResult>, llame al método de extensión DispatcherOperationWait. Al llamar a Task.Wait se producirá un interbloqueo. Para más información sobre el uso de una tarea 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 toman un delegado, Action, o 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 interfaz de usuario recibe la entrada, controla los eventos, dibuja la pantalla y ejecuta el código de la aplicación. La mayoría de aplicaciones usan un único subproceso de interfaz de usuario, aunque en algunos casos es recomendable usar varios. Analizaremos esto con un ejemplo más adelante.
El subproceso de interfaz de usuario pone en cola los elementos de trabajo dentro de un objeto denominado Dispatcher. Dispatcher selecciona los elementos de trabajo en función de la prioridad y ejecuta cada uno hasta que se completan. Todos los subprocesos de interfaz de usuario deben tener al menos un objeto Dispatcher, y cada Dispatcher puede ejecutar elementos de trabajo en un único subproceso.
El truco para compilar aplicaciones con capacidad de respuesta y fáciles de usar consiste en maximizar el rendimiento de Dispatcher reduciendo el tamaño de los elementos de trabajo. De esta forma, los elementos nunca quedan obsoletos en la cola de Dispatcher mientras esperan el procesamiento. Cualquier retraso perceptible 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 de algún servidor remoto? Normalmente, la respuesta es controlar la operación grande en un subproceso independiente, dejando libre el subproceso de interfaz de usuario para que tienden a elementos de la cola de Dispatcher. Una vez completada la operación grande, puede informar de su resultado al subproceso de interfaz de usuario para su presentación.
Históricamente, Windows solo permite obtener acceso a los elementos de la interfaz de usuario al subproceso que los ha creado. Esto significa que un subproceso en segundo plano a cargo de una tarea de ejecución prolongada no puede actualizar un cuadro de texto cuando finalice. Windows hace esto para garantizar la integridad de los componentes de la interfaz de usuario. Un cuadro de lista podría tener una apariencia extraña si un subproceso en segundo plano actualiza su contenido durante la presentación.
WPF tiene un mecanismo de exclusión mutua integrado que aplica esta coordinación. La mayoría de las clases de WPF derivan de DispatcherObject. Durante la construcción, un objeto DispatcherObject almacena una referencia a Dispatcher vinculado al subproceso actualmente en ejecución. En realidad, el objeto DispatcherObject se asocia con el subproceso que lo crea. Durante la ejecución del programa, un objeto DispatcherObject puede llamar a su método público VerifyAccess. VerifyAccess examina el Dispatcher asociado al subproceso actual y lo compara con la referencia de Dispatcher almacenada durante la construcción. Si no coinciden, VerifyAccess produce una excepción. VerifyAccess está pensado para que lo llamen al principio de todos los métodos pertenecientes a un objeto 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 la interfaz de usuario que realice una operación en su nombre. Para ello, registra un elemento de trabajo con Dispatcher del subproceso de la interfaz de usuario. La clase Dispatcher proporciona los métodos para registrar elementos de trabajo: Dispatcher.InvokeAsync, Dispatcher.BeginInvoke, y 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 de ejecutar el delegado. InvokeAsync
y BeginInvoke
son asincrónicos y devuelven inmediatamente.
Dispatcher ordena los elementos en la cola por prioridad. Se pueden especificar diez niveles al agregar un elemento a la cola de Dispatcher. Estas prioridades se mantienen en la enumeración DispatcherPriority.
Aplicación de un solo subproceso con un cálculo de ejecución prolongada
La mayoría de las interfaces gráficas de usuario (GUI) dedican una gran parte de su tiempo de inactividad a la espera de eventos que se generan en respuesta a interacciones del usuario. Con una programación adecuada, este tiempo de inactividad puede usarse 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 periódicamente al Dispatcher para procesar los eventos de entrada en espera antes de que queden obsoletos.
Se puede descargar una aplicación de muestra que muestre los conceptos de esta sección desde GitHub para C# o Visual Basic.
Considere el ejemplo siguiente:
Esta sencilla aplicación cuenta en orden ascendente a partir de tres, buscando números primos. Cuando el usuario hace clic en el botón Iniciar, la búsqueda comienza. Cuando el programa encuentra un número primo, actualiza la interfaz de usuario con su detección. En cualquier momento, 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 se controla la búsqueda completa dentro del controlador de eventos Click del botón, el subproceso de la interfaz de usuario nunca tendría oportunidad de controlar otros eventos. La interfaz de usuario no podría responder a la entrada ni procesar mensajes. No se volvería a dibujar nunca y tampoco respondería a los clics de botón.
Se podría realizar la búsqueda de números primos en un subproceso independiente, pero entonces sería necesario solucionar problemas de sincronización. Con un enfoque de un único subproceso, se puede actualizar directamente la etiqueta que muestra el número primo mayor encontrado.
Si se divide la tarea de cálculo en fragmentos manejables, se puede volver periódicamente al Dispatcher y procesar los eventos. Se le puede dar a WPF una oportunidad para que vuelva a dibujar y procese 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 uso del método InvokeAsync, se pueden programar las comprobaciones de números primos en la misma cola de la que se extraen los eventos de la interfaz de usuario. En el ejemplo, solamente se programa una comprobación de número primo cada vez. Una vez completada la comprobación de número primo, se programa la siguiente de manera inmediata. Esta comprobación se realiza solamente después de controlar los eventos de la interfaz de usuario pendientes.
Microsoft Word realiza la revisión ortográfica mediante este mecanismo. La revisión ortográfica se realiza en segundo plano usando 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 XAML mostrado en este artículo es de un proyecto C#. XAML de Visual Basic es ligeramente diferente al declarar la clase de respaldo 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 de la Button, el controlador StartStopButton_Click
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, el Dispatcher seleccionará el delegado para su ejecución.
Como se ha mencionado anteriormente, InvokeAsync es el miembro de Dispatcher que se usa para programar un delegado para la ejecución. En este caso, se elige la prioridad SystemIdle. Dispatcher ejecutará este delegado cuando no haya ningún evento importante por procesar. La capacidad de respuesta de la interfaz de usuario es más importante que la comprobación de números. También se pasa 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 directamente el bigPrime
TextBlock 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 hubiéramos decidido 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 para una combinación de subproceso/Dispatcher para administrar varias ventanas, pero a veces varios subprocesos hacen un mejor trabajo. Esto es especialmente cierto si hay alguna posibilidad de que una de las ventanas monopolice el subproceso.
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.
Las tres primeras ventanas de esta imagen comparten el mismo identificador de subproceso: 1. Las otras dos ventanas tienen identificadores de subprocesos diferentes: Nueve y 4. ¡¡Hay un color magenta girando!!glifo 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 subproceso.
Cuando se presiona el botónPausar, 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 de 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 tarea Task.Delay(TimeSpan) 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 controlador de eventos
SameThreadWindow_Click
muestra de forma inmediatamente una nueva ventana bajo el subproceso actual. El controlador de eventosNewThreadWindow_Click
crea un nuevo subproceso que comienza a ejecutar el métodoThreadStartingPoint
, 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 método
ThreadStartingPoint
es el punto inicial del nuevo subproceso. La nueva ventana se crea bajo el control de este subproceso. WPF crea automáticamente un nuevo System.Windows.Threading.Dispatcher para administrar el nuevo subproceso. Todo lo que hay que hacer para que la ventana sea funcional es iniciar 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 muestra 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
El control de 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 inmovilizarse. 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.BeginInvoke, o Dispatcher.Invoke, para insertar delegados en Dispatcher del subproceso de la interfaz de usuario. Finalmente, 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 modelo asincrónico basado en tarea (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 solo 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 un
Task
puede 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, se imita una llamada a procedimiento remoto que recupera la 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. 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 XAML para este ejemplo es bastante grande y no se proporciona en este artículo. Use los vínculos anteriores de GitHub para examinar la XAML. El XAML usa un solo botón para capturar el tiempo.
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 enumeran algunos de los detalles para 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
(oAsync
con Visual Basic). Un método "asincrónico" permite la suspensión del código cuando se llama a un método esperado, comoFetchWeatherFromServerAsync
. Esto se designa mediante laawait
palabra clave (oAwait
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
await FetchWeatherFromServerAsync();
a (oAwait FetchWeatherFromServerAsync()
con Visual Basic) hace que el código de se ejecute enFetchWeatherFromServerAsync
el subproceso de la interfaz de usuario, pero no se ejecuta en el distribuidor tiene tiempo para ejecutarlo, de forma similar a cómo funciona la aplicación de un solo subproceso con un ejemplo de cálculo de ejecución prolongada. Sin embargo, observe queawait 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.Captura de la información meteorológica
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, es posible simular el retraso de acceso de red haciendo que el nuevo subproceso espere 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 finalice el
Task.Delay
y hayamos seleccionado aleatoriamente nuestra previsión meteorológica, el estado meteorológico se devuelve al autor de la llamada.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 autor de la llamada de
Task.Run
, el controlador 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 "Capturar previsión".
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 de salto de tamaño constante
En las secciones siguientes se describen algunos de los detalles y los puntos de salto de tamaño que puede encontrar con multithreading.
Bombeo anidado
En ocasiones no es factible bloquear completamente el subproceso de la interfaz de usuario. Consideremos el método Show de la clase MessageBox. Show no devuelve hasta que el usuario hace clic en el botón Aceptar. Pero crea una ventana que debe tener un bucle de mensajes para poder ser interactiva. Mientras se espera a que el usuario haga clic en Aceptar, la ventana de la aplicación original no responde a la entrada del usuario. Pero sigue procesando los mensajes de pintura. La ventana original se vuelve a dibujar cuando se oculta y se muestra.
Algún subproceso debe estar a cargo de la ventana del cuadro de mensaje. WPF podría crear un nuevo subproceso para la ventana del 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 usa 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 y se inicia un nuevo bucle de mensajes para volver a dibujar la ventana de fondo y controlar la entrada de la ventana del 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 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 primario del mouse sobre la elipse, se ejecuta handler2
. Cuando finaliza handler2
, el evento se pasa al objeto Canvas, que usa handler1
para procesarlo. Esto solo sucede si handler2
no marca explícitamente el objeto como controlado.
Es posible que handler2
tarde mucho tiempo en procesar este evento. handler2
podría usar PushFrame para iniciar un bucle de mensajes anidado que no devuelva durante horas. Si handler2
no marca el evento como controlado cuando se complete este bucle de mensajes, el evento se pasa al árbol aunque sea muy antiguo.
Reentrada y bloqueo
El mecanismo de bloqueo del tiempo de ejecución del lenguaje común (CLR) no se comporta exactamente como uno podría imaginarse; cabría esperar que un hilo dejara de funcionar por completo al solicitar un bloqueo. En realidad, el subproceso continúa recibiendo y procesando mensajes de alta prioridad. Esto ayuda a evitar interbloqueos y a que la capacidad de respuesta de las interfaces sea mínima, pero introduce la posibilidad de errores sutiles. La gran mayoría del tiempo que no necesitas saber nada sobre esto, pero en raras circunstancias (normalmente implica mensajes de ventana Win32 o componentes COM STA), esto puede ser valioso saber.
La mayoría de las interfaces no se compilan con la seguridad para subprocesos en mente porque los desarrolladores trabajan bajo el supuesto de que no se tiene acceso a una interfaz de usuario desde más de un subproceso. En este caso, ese único subproceso puede realizar cambios en el entorno en momentos inesperados, provocando esos efectos adversos que el mecanismo de exclusión mutua de DispatcherObject supuestamente debe resolver. Considere el siguiente seudocódigo:
La mayoría de las veces que es lo correcto, pero hay veces en WPF donde la reentrada inesperada puede causar problemas. Por tanto, en determinados momentos clave, WPF llama DisableProcessing, que cambia la instrucción de bloqueo de ese subproceso para que use el bloqueo sin reentrada de WPF, en lugar del bloqueo habitual de CLR.
Entonces, ¿por qué ha elegido este comportamiento el equipo de CLR? 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 interfaz de usuario. Y aquí es donde reside el problema, porque un objeto COM STA creado en el subproceso de la interfaz de usuario solo se puede eliminar en el subproceso de la interfaz de usuario. CLR hace el equivalente de un BeginInvoke (en este caso, mediante el SendMessage
de Win32 ). 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 tanto, el equipo de CLR ha tomado la difícil decisión de hacer que los bloqueos funcionen 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
.NET Desktop feedback