Modelo de threading

O Windows Presentation Foundation (WPF) foi projetado para salvar 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 multi-threaded são complexos e difíceis de serem depurados, deve-se evitá-los quando existem soluções single-threaded.

Não importa o quão bem arquitetada, no entanto, nenhuma estrutura de interface do usuário é capaz de fornecer uma solução de thread único para cada tipo de problema. 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 alguns materiais 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 threading usando o InvokeAsync método para chamadas assíncronas. O InvokeAsync método usa um ou como um parâmetro e retorna um ActionDispatcherOperation ou Func<TResult>DispatcherOperation<TResult>, que tem uma Task propriedade. Você pode usar a await palavra-chave com o ou o TaskDispatcherOperation associado . Se você precisar aguardar de forma síncrona o Task que é retornado por um DispatcherOperation ou , chame DispatcherOperation<TResult>o método de DispatcherOperationWait extensão. A chamada Task.Wait resultará em um 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 Invoke método, que também tem sobrecargas que leva um delegado, Actionou Func<TResult> parâmetro.

Visão geral e o despachante

Normalmente, os aplicativos WPF começam com dois threads: um para manipular 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 Dispatcherum objeto chamado . O Dispatcher seleciona itens de trabalho em uma base de prioridade e executa cada um até a conclusão. Cada thread de interface do usuário deve ter pelo menos um , e cada Dispatcher um pode executar itens de trabalho em exatamente um Dispatcherthread.

O truque para criar aplicativos responsivos e fáceis de usar é maximizar a Dispatcher taxa de transferência, mantendo os itens de trabalho pequenos. Dessa forma, os itens nunca ficam obsoletos na fila Dispatcher aguardando processamento. Qualquer atraso perceptí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 código envolver um cálculo grande ou precisar consultar um banco de dados em algum servidor remoto? Normalmente, a resposta é manipular a operação grande 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 de segundo plano responsável por uma tarefa de execução longa não pode atualizar uma caixa de texto quando ele é concluído. O Windows faz isso para garantir a integridade dos componentes da interface do usuário. Uma caixa de listagem poderá ter uma aparência estranha se seu conteúdo for atualizado por um thread de segundo plano durante a pintura.

O WPF tem um mecanismo interno de exclusão mútua 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 vinculado ao Dispatcher thread em execução no momento. Com efeito, o associa ao thread que o DispatcherObject cria. Durante a execução do programa, um DispatcherObject pode chamar seu método público VerifyAccess . VerifyAccess examina o associado ao thread atual e o Dispatcher compara com a referência armazenada durante a Dispatcher construção. Se não combinarem, VerifyAccess abre uma exceção. VerifyAccess destina-se a ser chamado no início de cada método pertencente a um DispatcherObject.

Se apenas um thread puder modificar a interface do usuário, como os threads em segundo plano interagem com o usuário? Um thread em segundo plano pode pedir ao thread da interface do usuário para executar uma operação em seu nome. Ele faz isso registrando um item de trabalho com o Dispatcher thread da interface do usuário. A Dispatcher classe fornece os métodos para registrar itens de trabalho: Dispatcher.InvokeAsync, Dispatcher.BeginInvokee 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 são assíncronos e BeginInvoke 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. Essas prioridades são mantidas na DispatcherPriority enumeração.

Aplicativo de thread único com um cálculo de longa execução

A maioria das interfaces gráficas do usuário (GUIs) passa uma grande parte de seu tempo ociosa enquanto aguarda eventos que são 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 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 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:

Screenshot that shows threading of prime numbers.

Este aplicativo simples faz uma contagem ascendente a partir do três, pesquisando os números primos. Quando o usuário clica no botão Iniciar, a pesquisa é iniciada. Quando o programa encontra um primo, ele atualiza a interface do usuário com sua descoberta. A qualquer momento, o usuário pode parar a pesquisa.

Embora seja simples o suficiente, a pesquisa de números primos poderá continuar para sempre, o que apresenta algumas dificuldades. Se manipulássemos toda a pesquisa dentro do manipulador de eventos click do botão, nunca daria ao thread da interface do usuário a chance de manipular outros eventos. A interface do usuário não poderá responder a mensagens de entrada ou de processo. Ele nunca redesenhará e nunca responderá aos cliques do botão.

Poderíamos realizar a pesquisa de números primos em um thread separado, mas precisaríamos lidar com problemas de sincronização. Com uma abordagem single-threaded, podemos atualizar diretamente o rótulo que lista o maior primo encontrado.

Se dividirmos a tarefa de cálculo em partes gerenciáveis, poderemos retornar periodicamente aos Dispatcher eventos e processar. Podemos dar ao WPF uma oportunidade de repintar e processar a entrada.

A melhor maneira de dividir o tempo de processamento entre o cálculo e o tratamento de eventos é gerenciar o Dispatchercálculo a partir do . Usando o InvokeAsync método, podemos agendar verificações de número primo na mesma fila da qual os eventos da interface do usuário são extraídos. Em nosso exemplo, agendamos somente uma única verificação de números primos por vez. Depois que a verificação de números primos for concluída, agendaremos a próxima verificação imediatamente. Essa verificação prossegue somente depois que os eventos de interface do usuário pendentes forem manipulados.

Screenshot that shows the dispatcher queue.

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 thread da interface do usuário. 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#. Visual Basic XAML é 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 à Dispatcher fila. Algum tempo depois que esse manipulador de eventos tiver concluído seu trabalho, o selecionará o Dispatcher 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á esse 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 que representa 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

Esse método verifica se o próximo número ímpar é um primo. Se for prime, o método atualiza diretamente o bigPrimeTextBlock 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. Demonstraremos 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 de 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 Windows Explorer funciona dessa maneira. 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 esse conceito com o exemplo a seguir.

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

As três janelas superiores desta imagem compartilham o mesmo identificador de thread: 1. As outras duas janelas têm identificadores de thread diferentes: Nine e 4. Há um glifo giratório !️ de cor magenta no canto superior direito de cada janela.

Este exemplo contém uma janela com um glifo giratório‼️, 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 Pausar seja pressionado, o que pausa o fio 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

A seguir, alguns dos detalhes a serem observados:

  • 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. O NewThreadWindow_Click manipulador de eventos cria um novo thread que começa a executar o ThreadStartingPoint método, que por sua vez mostra uma nova janela, conforme descrito no próximo marcador.

    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. WPF cria automaticamente um novo para gerenciar o novo System.Windows.Threading.Dispatcher thread. Tudo o que temos que 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

A manipulação de operações de bloqueio em um aplicativo gráfico pode ser difícil. Não queremos chamar métodos de bloqueio de manipuladores de eventos porque o aplicativo parece congelar. O exemplo anterior criou novas janelas em seu próprio thread, permitindo que cada janela fosse executada independente uma da outra. Embora possamos criar um novo thread com o , torna-se difícil sincronizar o novo thread com System.Windows.Threading.Dispatchero thread principal da interface do usuário depois que o trabalho for concluído. Como o novo thread não pode modificar a interface do usuário diretamente, precisamos usar Dispatcher.InvokeAsync, ou Dispatcher.Invoke, Dispatcher.BeginInvokepara inserir delegados no Dispatcher thread da interface do usuário. 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 tarefa (TAP). Ele se baseia nos Task tipos e Task<TResult> no System.Threading.Tasks namespace, que são usados para representar operações assíncronas. O 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 nesse padrão:

  • O chamador de um Task pode optar por executar o código de forma assíncrona ou síncrona.
  • O progresso pode ser relatado a Taskpartir do .
  • 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 do tempo. 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 de 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 é suspenso e não bloqueia o restante do thread da interface do usuário. 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.

A diagram that demonstrates the workflow of the example app.

Um diagrama que demonstra 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 é muito grande e não é fornecido neste artigo. Use os links anteriores do GitHub para procurar o XAML. O XAML usa um único botão para buscar o clima.

Considere o code-behind para o 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

Veja a seguir alguns dos detalhes a serem observados.

  • O manipulador de eventos button

    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 (ou Async com async o Visual Basic). Um método "assíncrono" permite a suspensão do código quando um método aguardado, como FetchWeatherFromServerAsync, é chamado. Isso é designado pela palavra-chave (ou Await com Visual await Basic). Até que o termo termine, o FetchWeatherFromServerAsync 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 seja concluída após a qual o controle é retornado ao chamador.

    Os métodos aguardados utilizam o contexto de threading do método atual, que com o manipulador de botões, é o thread da interface do usuário. Isso significa que a chamada await FetchWeatherFromServerAsync(); (ou Await FetchWeatherFromServerAsync() com o Visual Basic) faz com que o código seja executado no FetchWeatherFromServerAsync thread da interface do usuário, mas não é executado no dispatcher tem tempo para executá-lo, semelhante a como o aplicativo de thread único com um exemplo de cálculo de longa execução opera. No entanto, observe que await Task.Run é usado. Isso cria um novo thread no pool de threads para a tarefa designada em vez do thread atual. Então FetchWeatherFromServerAsync corre em seu próprio fio.

  • Buscando o clima

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

    Para simplificar, 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 de interface do usuário original ainda está em execução e respondendo a eventos de 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 estivesse pausado ou atrasado, a animação não seria mostrada e você não poderia interagir com a janela.

    Quando o estiver concluído, e selecionarmos aleatoriamente nossa previsão do tempo, o Task.Delay status 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 for concluída e o thread da interface do usuário tiver tempo, o chamador do manipulador de eventos , o manipulador de eventos do botão, Task.Runserá retomado. O resto do método interrompe a animação do relógio e escolhe uma imagem para descrever o clima. Ele exibe essa 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 ShowMessageBox método da aula. 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 mensagens para ser interativa. Enquanto aguardamos até que o usuário clique em OK, a janela do aplicativo original não responde à entrada do usuário. No entanto, ele continua processando mensagens de pintura. A janela original se redesenha quando é coberta e revelada.

Screenshot that shows a MessageBox with an OK button

Algum thread deve ser responsável pela janela da caixa de mensagem. O WPF poderia criar um novo thread apenas para a janela da caixa de mensagem, mas esse thread seria incapaz de pintar os elementos desabilitados na janela original (lembre-se da discussão anterior de 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 loop de mensagem aninhado terminar, a execução será retomada após a chamada original PushFrame .

Nesse caso, mantém o contexto do programa na chamada para , PushFrame e ele inicia um novo loop de mensagem para repintar a janela de plano de fundo e manipular a entrada para MessageBox.Showa janela da caixa de mensagem. Quando o usuário clica em OK e limpa a janela pop-up, o loop aninhado sai e o controle é retomado após a chamada para Show.

Eventos roteados obsoletos

O sistema de eventos roteados no WPF notifica árvores inteiras quando os 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 handler2 a conclusão, o evento é passado para o objeto, que o Canvas usa handler1 para processá-lo. Isso acontece somente se handler2 não marcar explicitamente o objeto de evento como manipulado.

É possível que handler2 isso leve muito tempo processando esse 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 da árvore mesmo que seja muito antigo.

Reentrância e travamento

O mecanismo de bloqueio do Common Language Runtime (CLR) não se comporta exatamente como se poderia imaginar; pode-se esperar que um thread pare completamente de operar ao solicitar um bloqueio. Na realidade, o thread continua recebendo e processando mensagens de alta prioridade. Isso ajuda a impedir bloqueios e torna a interface minimamente dinâmica, 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 de 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. Nesse 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 deve resolver. Considere o seguinte pseudocódigo:

Diagram that shows threading reentrancy.

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 determinados momentos-chave, o WPF chama DisableProcessing, o que altera a instrução de bloqueio para esse thread para usar o bloqueio sem reentrância do WPF, em vez do bloqueio CLR usual.

Então, por que a equipe do CLR escolheu esse comportamento? Isso tinha a ver com objetos COM STA e com o thread de finalização. Quando um objeto é coletado lixo, seu Finalize método é executado no thread do finalizador dedicado, não no thread da interface do usuário. E aí está o problema, porque um objeto COM STA 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 um 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 fez o duro apelo para que as travas funcionassem da maneira que funcionam.

A tarefa do WPF é evitar a reentrância inesperada sem reintroduzir o vazamento de memória, e é por isso que não bloqueamos a reentrância em todos os lugares.

Confira também