Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Windows Presentation Foundation (WPF) foi concebido para poupar os desenvolvedores das dificuldades de threading. Como resultado, a maioria dos desenvolvedores do WPF não escreve uma interface que usa mais de um thread. Como os programas multithreaded são complexos e difíceis de depurar, eles devem ser evitados quando existem soluções single-threaded.
Nenhuma estrutura de interface do utilizador é capaz de fornecer uma solução monothread para cada tipo de problema, não importa quão bem arquitetada esteja. O WPF chega perto, mas ainda há situações em que vários threads melhoram a capacidade de resposta da interface do usuário (UI) ou o desempenho do aplicativo. Depois de discutir algum material de fundo, este artigo explora algumas dessas situações e, em seguida, conclui com uma discussão de alguns detalhes de nível inferior.
Observação
Este tópico discute o encadeamento usando o método InvokeAsync para chamadas assíncronas. O InvokeAsync
método usa um Action ou Func<TResult> como um parâmetro e retorna um DispatcherOperation ou DispatcherOperation<TResult>, que tem uma Task propriedade. Você pode usar a await
palavra-chave com o DispatcherOperation ou com o Task associado. Caso necessite aguardar sincronicamente pelo Task que é retornado por um DispatcherOperation ou DispatcherOperation<TResult>, chame o método de extensão DispatcherOperationWait. A chamada Task.Wait resultará num impasse. Para obter mais informações sobre como usar um Task para executar operações assíncronas, consulte Programação assíncrona baseada em tarefas.
Para fazer uma chamada síncrona, use o método Invoke, que também tem sobrecargas que levam um delegado, ou um parâmetro Action ou Func<TResult>.
Visão geral e o despachante
Normalmente, os aplicativos WPF começam com dois threads: um para lidar com a renderização e outro para gerenciar a interface do usuário. O thread de renderização é efetivamente executado oculto em segundo plano enquanto o thread da interface do usuário recebe entrada, manipula eventos, pinta a tela e executa o código do aplicativo. A maioria dos aplicativos usa um único thread de interface do usuário, embora em algumas situações seja melhor usar vários. Discutiremos isso com um exemplo mais adiante.
O thread da interface do usuário enfileira itens de trabalho dentro de um objeto chamado Dispatcher. O Dispatcher seleciona itens de trabalho em uma base de prioridade e executa cada um até a conclusão. Cada thread de interface do utilizador deve ter pelo menos um Dispatcher, e cada Dispatcher pode executar itens de trabalho em exatamente um thread.
O truque para criar aplicativos responsivos e fáceis de usar é maximizar a Dispatcher taxa de transferência, mantendo os itens de trabalho pequenos. Desta forma, os itens nunca ficam obsoletos sentados na fila Dispatcher à espera de processamento. Qualquer atraso percetível entre a entrada e a resposta pode frustrar um usuário.
Como, então, os aplicativos WPF devem lidar com grandes operações? E se o seu código envolver um cálculo grande ou precisar consultar um banco de dados em algum servidor remoto? Normalmente, a resposta é lidar com a grande operação em um thread separado, deixando o thread da interface do usuário livre para atender aos itens na Dispatcher fila. Quando a operação grande estiver concluída, ela poderá relatar seu resultado de volta ao thread da interface do usuário para exibição.
Historicamente, o Windows permite que os elementos da interface do usuário sejam acessados apenas pelo thread que os criou. Isso significa que um thread em segundo plano encarregado de alguma tarefa de longa execução não pode atualizar uma caixa de texto quando ela for concluída. O Windows faz isso para garantir a integridade dos componentes da interface do usuário. Uma caixa de listagem poderia parecer estranha se seu conteúdo fosse atualizado por um fio de fundo durante a pintura.
O WPF tem um mecanismo de exclusão mútua integrado que impõe essa coordenação. A maioria das classes no WPF deriva de DispatcherObject. Na construção, um DispatcherObject armazena uma referência ao Dispatcher ligado ao thread que está atualmente em execução. Com efeito, o DispatcherObject associa-se ao thread que o cria. Durante a execução do programa, um DispatcherObject pode chamar seu método público VerifyAccess . VerifyAccess examina o Dispatcher associado com o thread atual e o compara com a referência armazenada durante a Dispatcher construção. Se não corresponderem, VerifyAccess lança uma exceção. VerifyAccess destina-se a ser chamado no início de cada método pertencente a um DispatcherObject.
Se apenas um thread pode modificar a interface do usuário, como os threads em segundo plano interagem com o usuário? Um thread em segundo plano pode solicitar que o thread da interface do usuário execute uma operação em seu nome. Ele faz isso registando um item de trabalho na thread da interface do utilizador Dispatcher. A Dispatcher classe fornece os métodos para registrar itens de trabalho: Dispatcher.InvokeAsync, Dispatcher.BeginInvoke, e Dispatcher.Invoke. Esses métodos agendam um delegado para execução.
Invoke
é uma chamada síncrona, ou seja, não retorna até que o thread da interface do usuário realmente termine de executar o delegado.
InvokeAsync
e BeginInvoke
são assíncronos e retornam imediatamente.
O Dispatcher ordena os elementos em sua fila por prioridade. Há dez níveis que podem ser especificados ao adicionar um elemento à Dispatcher fila. Estas prioridades são mantidas na DispatcherPriority enumeração.
Aplicação de thread único com um cálculo de longa duração
A maioria das interfaces gráficas do usuário (GUIs) passa uma grande parte do tempo ocioso enquanto aguarda eventos gerados em resposta às interações do usuário. Com uma programação cuidadosa, esse tempo ocioso pode ser usado de forma construtiva, sem afetar a capacidade de resposta da interface do usuário. O modelo de threading do WPF não permite que a entrada interrompa uma operação que está acontecendo no thread da interface do usuário. Isso significa que você deve ter certeza de retornar ao Dispatcher periodicamente para processar eventos de entrada pendentes antes que eles fiquem obsoletos.
Um aplicativo de exemplo demonstrando os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic.
Considere o seguinte exemplo:
Esta aplicação simples conta a partir de três, procurando números primos. Quando o usuário clica no botão Iniciar , a pesquisa começa. Quando o programa encontra um prime, ele atualiza a interface do usuário com sua descoberta. A qualquer momento, o usuário pode parar a pesquisa.
Apesar de bastante simples, a pesquisa de números primos pode continuar para sempre, o que apresenta algumas dificuldades. Se tratássemos de toda a pesquisa dentro do manipulador de eventos de clique do botão, nunca daríamos ao thread da interface do usuário a chance de manipular outros eventos. A interface do usuário seria incapaz de responder a mensagens de entrada ou processo. Nunca repintaria e nunca responderia a cliques de botões.
Poderíamos conduzir a pesquisa de números primos em um thread separado, mas precisaríamos lidar com problemas de sincronização. Com uma abordagem de um único encadeamento, podemos atualizar diretamente o rótulo que lista o maior número primo encontrado.
Se dividirmos a tarefa de cálculo em partes gerenciáveis, podemos retornar periodicamente aos Dispatcher eventos e processá-los. Podemos oferecer ao WPF uma oportunidade de repintar e processar a entrada.
A melhor maneira de dividir o tempo de processamento entre o cálculo e a manipulação de eventos é gerenciar o cálculo a partir do Dispatcher. Usando o InvokeAsync método, podemos agendar verificações de números primos na mesma fila da qual os eventos da interface do usuário são extraídos. No nosso exemplo, agendamos apenas uma verificação de número primo de cada vez. Depois que a verificação do número primo estiver concluída, agendamos a próxima verificação imediatamente. Essa verificação prossegue somente depois que os eventos pendentes da interface do usuário forem tratados.
O Microsoft Word realiza a verificação ortográfica usando esse mecanismo. A verificação ortográfica é feita em segundo plano usando o tempo ocioso do tópico da interface de utilizador. Vamos dar uma olhada no código.
O exemplo a seguir mostra o XAML que cria a interface do usuário.
Importante
O XAML mostrado neste artigo é de um projeto C#. O XAML do Visual Basic é ligeiramente diferente ao declarar a classe de suporte para o 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>
O exemplo a seguir mostra o code-behind.
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
Além de atualizar o texto no Button, o StartStopButton_Click
manipulador é responsável por agendar a primeira verificação de número primo adicionando um delegado à fila Dispatcher. Algum tempo depois que esse manipulador de eventos tiver concluído seu trabalho, o Dispatcher selecionará o delegado para execução.
Como mencionamos anteriormente, InvokeAsync é o Dispatcher membro usado para agendar um delegado para execução. Neste caso, escolhemos a SystemIdle prioridade. O Dispatcher executará este delegado somente quando não houver eventos importantes para processar. A capacidade de resposta da interface do usuário é mais importante do que a verificação de números. Também passamos um novo delegado representando a rotina de verificação 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 verifica se o próximo número ímpar é primo. Se for prime, o método atualiza diretamente o bigPrime
TextBlock para refletir sua descoberta. Podemos fazer isso porque o cálculo está ocorrendo no mesmo thread que foi usado para criar o controle. Se tivéssemos optado por usar um thread separado para o cálculo, teríamos que usar um mecanismo de sincronização mais complicado e executar a atualização no thread da interface do usuário. Vamos demonstrar essa situação a seguir.
Várias janelas, vários threads
Alguns aplicativos WPF exigem várias janelas de nível superior. É perfeitamente aceitável que uma combinação Thread/Dispatcher gerencie várias janelas, mas às vezes vários threads fazem um trabalho melhor. Isso é especialmente verdadeiro se houver alguma chance de que uma das janelas monopolize o thread.
O Explorador do Windows funciona desta forma. Cada nova janela do Explorer pertence ao processo original, mas é criada sob o controle de um thread independente. Quando o Explorer deixa de responder, como ao procurar recursos de rede, outras janelas do Explorer continuam a ser responsivas e utilizáveis.
Podemos demonstrar este conceito com o seguinte exemplo.
As três janelas superiores desta imagem partilham o mesmo identificador de thread: 1. As outras duas janelas têm identificadores de thread diferentes: Nine e 4. Há um glifo rotativo !️ colorido magenta no canto superior direito de cada janela.
Este exemplo contém uma janela com um glifo rotativo ‼️
, um botão Pausar e dois outros botões que criam uma nova janela sob o thread atual ou em um novo thread. O ‼️
glifo está constantemente girando até que o botão Pause seja pressionado, o que pausa o thread por cinco segundos. Na parte inferior da janela, o identificador de thread é exibido.
Quando o botão Pausar é pressionado, todas as janelas sob o mesmo thread deixam de responder. Qualquer janela sob um thread diferente continua a funcionar normalmente.
O exemplo a seguir é o XAML para a janela:
<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>
O exemplo a seguir mostra o code-behind.
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
Seguem-se alguns dos detalhes a observar:
A Task.Delay(TimeSpan) tarefa é usada para fazer com que o thread atual pause por cinco segundos quando o botão Pausar é pressionado.
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
O
SameThreadWindow_Click
manipulador de eventos mostra imediatamente uma nova janela sob o thread atual. ONewThreadWindow_Click
manipulador de eventos cria um novo thread que começa a executar oThreadStartingPoint
método, que, por sua vez, mostra uma nova janela, conforme descrito no próximo ponto.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
O
ThreadStartingPoint
método é o ponto de partida para o novo thread. A nova janela é criada sob o controle deste thread. O WPF cria automaticamente um novo System.Windows.Threading.Dispatcher para gerenciar o novo thread. Tudo o que temos de fazer para tornar a janela funcional é iniciar o 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
Um aplicativo de exemplo demonstrando os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic.
Manipular uma operação de bloqueio com Task.Run
Lidar com operações de bloqueio em um aplicativo gráfico pode ser difícil. Não queremos chamar métodos bloqueadores a partir de manipuladores de eventos porque a aplicação parece congelar. O exemplo anterior criou novas janelas em seu próprio thread, permitindo que cada janela fosse executada independentemente uma da outra. Embora possamos criar uma nova thread com System.Windows.Threading.Dispatcher, torna-se difícil sincronizar a nova thread com a thread principal do UI após a conclusão do trabalho. Como o novo thread não pode modificar a interface do utilizador diretamente, precisamos usar Dispatcher.InvokeAsync, Dispatcher.BeginInvoke ou Dispatcher.Invoke, para inserir delegados no Dispatcher thread da interface do utilizador. Eventualmente, esses delegados são executados com permissão para modificar elementos da interface do usuário.
Há uma maneira mais fácil de executar o código em um novo thread enquanto sincroniza os resultados, o padrão assíncrono baseado em tarefas (TAP). É baseado nos tipos Task e Task<TResult> no namespace System.Threading.Tasks
, que são usados para representar operações assíncronas. A TAP usa um único método para representar o início e a conclusão de uma operação assíncrona. Existem alguns benefícios neste padrão:
- O chamador de um
Task
pode optar por executar o código de forma assíncrona ou síncrona. - Os progressos podem ser comunicados a partir do
Task
. - O código de chamada pode suspender a execução e aguardar o resultado da operação.
Exemplo de Task.Run
Neste exemplo, simulamos uma chamada de procedimento remoto que recupera uma previsão meteorológica. Quando o botão é clicado, a interface do usuário é atualizada para indicar que a busca de dados está em andamento, enquanto uma tarefa é iniciada para imitar a busca da previsão do tempo. Quando a tarefa é iniciada, o código do manipulador de eventos do botão é suspenso até que a tarefa seja concluída. Após a conclusão da tarefa, o código do manipulador de eventos continua a ser executado. O código está suspenso e não bloqueia o resto da UI thread. O contexto de sincronização do WPF lida com a suspensão do código, o que permite que o WPF continue a ser executado.
Um diagrama demonstrando o fluxo de trabalho do aplicativo de exemplo. O aplicativo tem um único botão com o texto "Buscar previsão". Há uma seta apontando para a próxima fase do aplicativo depois que o botão é pressionado, que é uma imagem de relógio colocada no centro do aplicativo indicando que o aplicativo está ocupado buscando dados. Depois de algum tempo, o aplicativo retorna com uma imagem do sol ou de nuvens de chuva, dependendo do resultado dos dados.
Um aplicativo de exemplo demonstrando os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic. O XAML para este exemplo é bastante grande e não é fornecido neste artigo. Use os links anteriores do GitHub para navegar pelo XAML. O XAML usa um único botão para obter o tempo.
Considere o código subjacente do 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 seguir estão alguns dos detalhes a serem observados.
Manipulador de eventos do botão
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 o manipulador de eventos foi declarado com
async
(ouAsync
com Visual Basic). Um método "async" permite a suspensão do código quando um método aguardado, comoFetchWeatherFromServerAsync
, é chamado. Isso é designado pela palavra-chaveawait
(ouAwait
com o Visual Basic). Até aFetchWeatherFromServerAsync
conclusão, o código do manipulador do botão é suspenso e o controle é retornado ao chamador. Isso é semelhante a um método síncrono, exceto que um método síncrono aguarda que cada operação no método termine, após o qual o controle é retornado ao chamador.Os métodos aguardados utilizam o contexto de thread do método atual, juntamente com o manipulador de botão, que é o thread da interface do usuário. Isso significa que chamar
await FetchWeatherFromServerAsync();
(ouAwait FetchWeatherFromServerAsync()
no Visual Basic) faz com que o código emFetchWeatherFromServerAsync
seja executado na thread da interface do usuário, mas só é executado quando o dispatcher tem tempo para fazê-lo, semelhante a como o exemplo Aplicativo de thread único com cálculo de longa duração opera. No entanto, observe queawait Task.Run
é usado. Isso cria um novo thread no pool de threads para a tarefa designada em vez do thread atual. EntãoFetchWeatherFromServerAsync
corre na sua própria thread.Buscando o tempo
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, não temos nenhum código de rede neste exemplo. Em vez disso, simulamos o atraso do acesso à rede colocando nosso novo thread em suspensão por quatro segundos. Nesse período, o thread da interface do usuário original ainda está em execução e respondendo a eventos da interface do usuário, enquanto o manipulador de eventos do botão é pausado até que o novo thread seja concluído. Para demonstrar isso, deixamos uma animação em execução e você pode redimensionar a janela. Se o thread da interface do usuário fosse pausado ou atrasado, a animação não seria mostrada e você não poderia interagir com a janela.
Quando o
Task.Delay
estiver terminado e tivermos selecionado aleatoriamente a previsão do tempo, o estado do tempo será retornado ao chamador.Atualizando a interface do usuário
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
Quando a tarefa é concluída e o thread da interface do usuário tem tempo, o chamador do manipulador de eventos do
Task.Run
botão é retomado. O resto do método pára a animação do relógio e escolhe uma imagem para descrever o estado do tempo. Ele exibe esta imagem e habilita o botão "buscar previsão".
Um aplicativo de exemplo demonstrando os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic.
Detalhes técnicos e pontos de tropeço
As seções a seguir descrevem alguns dos detalhes e pontos de tropeço que você pode encontrar com o multithreading.
Bombeamento aninhado
Às vezes, não é viável bloquear completamente o thread da interface do usuário. Vamos considerar o método Show da classe MessageBox. Show não retorna até que o usuário clique no botão OK. No entanto, ele cria uma janela que deve ter um loop de mensagem para ser interativa. Enquanto aguardamos que o usuário clique em OK, a janela original do aplicativo não responde à entrada do usuário. No entanto, continua a processar mensagens relacionadas com pintura. A janela original redesenha-se quando é coberta e depois revelada.
Algum thread deve ser responsável pela janela da caixa de mensagem. WPF poderia criar um novo thread apenas para a janela da caixa de mensagem, mas esse thread seria incapaz de pintar os elementos desativados na janela original (lembre-se da discussão anterior sobre exclusão mútua). Em vez disso, o WPF usa um sistema de processamento de mensagens aninhado. A Dispatcher classe inclui um método especial chamado PushFrame, que armazena o ponto de execução atual de um aplicativo e, em seguida, inicia um novo loop de mensagem. Quando o ciclo de mensagens aninhado termina, a execução é retomada após a chamada original PushFrame.
Nesse caso, PushFrame mantém o contexto do programa na chamada para MessageBox.Show, e inicia um novo loop de mensagem para repintar a janela de plano de fundo e manipular a entrada para a janela da caixa de mensagem. Quando o utilizador clica em OK e fecha a janela pop-up, o loop aninhado termina e o controlo é retomado após a chamada para Show.
Eventos enviados desatualizados
O sistema de eventos roteado no WPF notifica árvores inteiras quando eventos são gerados.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
Quando o botão esquerdo do mouse é pressionado sobre a elipse, handler2
é executado. Após a conclusão de handler2
, o evento é encaminhado para o objeto Canvas, que utiliza handler1
para o processar. Isso acontece somente se handler2
não marcar explicitamente o objeto de evento como manipulado.
É possível que handler2
demore muito tempo a processar este evento.
handler2
pode ser usado PushFrame para iniciar um loop de mensagem aninhado que não retorna por horas. Se handler2
não marcar o evento como manipulado quando esse loop de mensagem for concluído, o evento será passado para cima na árvore, mesmo que seja muito antigo.
Reentrância e bloqueio
O mecanismo de bloqueio do Common Language Runtime (CLR) não se comporta exatamente como se poderia imaginar; pode-se esperar que um thread cesse completamente a operação ao solicitar um bloqueio. Na verdade, o thread continua a receber e processar mensagens de alta prioridade. Isso ajuda a evitar impasses e tornar as interfaces minimamente responsivas, mas introduz a possibilidade de bugs sutis. Na grande maioria das vezes você não precisa saber nada sobre isso, mas em circunstâncias raras (geralmente envolvendo mensagens de janela Win32 ou componentes COM STA) isso pode valer a pena saber.
A maioria das interfaces não é criada com a segurança do thread em mente porque os desenvolvedores trabalham sob a suposição de que uma interface do usuário nunca é acessada por mais de um thread. Neste caso, esse único fio pode fazer mudanças ambientais em momentos inesperados, causando aqueles efeitos nocivos que o mecanismo de DispatcherObject exclusão mútua deveria resolver. Considere o seguinte pseudocódigo:
Na maioria das vezes isso é a coisa certa, mas há momentos no WPF em que essa reentrância inesperada pode realmente causar problemas. Assim, em certos momentos críticos, o WPF chama DisableProcessing, o que altera a instrução de bloqueio desse thread para usar um bloqueio que evita reentrância do WPF, em vez do bloqueio usual do CLR.
Então, por que a equipe do CLR escolheu esse comportamento? Tinha a ver com objetos COM STA e o thread de finalização. Quando um objeto é coletado como lixo, seu Finalize
método é executado no thread do finalizador dedicado, não no thread da interface do usuário. E aí reside o problema, porque um objeto STA COM que foi criado no thread da interface do usuário só pode ser descartado no thread da interface do usuário. O CLR faz o equivalente a a BeginInvoke (neste caso, usando Win32's SendMessage
). Mas se o thread da interface do usuário estiver ocupado, o thread do finalizador será paralisado e o objeto COM STA não poderá ser descartado, o que criará um grave vazamento de memória. Então, a equipe do CLR tomou a difícil decisão para fazer com que os bloqueios funcionassem da maneira como funcionam.
A tarefa do WPF é evitar a reentrância inesperada sem causar vazamento de memória, e é por isso que não bloqueamos a reentrância por toda parte.
Ver também
.NET Desktop feedback