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 Dispatcher. Dispatcher 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 VerifyAccess. VerifyAccess 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:
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.
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.
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 MessageBox. Show 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.
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:
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