Compartilhar via


Modelo de threading

O WPF (Windows Presentation Foundation) 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 multithreaded são complexos e difíceis de depurar, eles devem ser evitados quando existem soluções de thread único.

Não importa o quão bem arquiteta, no entanto, nenhuma estrutura de interface do usuário é capaz de fornecer uma solução de thread único para todos os tipos de problema. O WPF se aproxima, mas ainda há situações em que vários threads melhoram a capacidade de resposta da interface do usuário ou o desempenho do aplicativo. Depois de discutir algum material em segundo plano, este artigo explora algumas dessas situações e, em seguida, conclui com uma discussão sobre 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 Action ou Func<TResult> como 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 o associado Task. Se você precisar aguardar de forma síncrona para o Task que é retornado por um DispatcherOperation ou DispatcherOperation<TResult>, chame o método de DispatcherOperationWait extensão. A chamada Task.Wait resultará em um deadlock. Para obter mais informações sobre como usar uma Task para executar operações assíncronas, consulte a programação assíncrona baseada em tarefas.

Para fazer uma chamada síncrona, use o Invoke método, que também tem sobrecargas que usam um delegado Actionou Func<TResult> parâmetro.

Visão geral e o dispatcher

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 é executado efetivamente oculto em segundo plano enquanto o thread de 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 posteriormente.

As filas de thread de interface do usuário funcionam itens dentro de um objeto chamado Dispatcher. Os Dispatcher itens de trabalho são selecionados com prioridade e executam cada um deles até a conclusão. Cada thread de interface do usuário deve ter pelo menos um Dispatcher, e cada Dispatcher pode executar itens de trabalho em exatamente uma thread.

O truque para criar aplicativos responsivos e amigáveis é maximizar a Dispatcher taxa de transferência mantendo os itens de trabalho pequenos. Dessa forma, os itens nunca ficam obsoletos sentados na Dispatcher fila aguardando processamento. Qualquer atraso percebível entre entrada e resposta pode frustrar um usuário.

Como 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 é 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 é concluída, ela pode relatar seu resultado de volta para o thread de 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 responsável por alguma tarefa de execução longa 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 uma thread de segundo plano durante o processo de renderização.

O WPF tem um mecanismo de exclusão mútua interno 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 vinculado ao thread atual. De fato, o DispatcherObject se associa à thread que o cria. Durante a execução do programa, um DispatcherObject pode chamar seu método público VerifyAccess . VerifyAccess examina o Dispatcher thread atual associado e o compara à referência armazenada durante a Dispatcher construção. Se eles não corresponderem, VerifyAccess gerará 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 de interface do usuário para executar uma operação em seu nome. Ele faz isso registrando um item de trabalho com o Dispatcher da 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 a execução. Invoke é uma chamada síncrona – ou seja, ela não retorna até que o thread da interface do usuário realmente conclua a execução do delegado. InvokeAsync e BeginInvoke são assíncronos e retornam imediatamente.

O Dispatcher organiza 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 com thread único com um cálculo de execução prolongada

A maioria das GUIs (interfaces gráficas do usuário) gasta grande parte de seu tempo ocioso enquanto aguarda eventos gerados em resposta às interações do usuário. Com a programação cuidadosa, esse tempo ocioso pode ser usado construtivamente, 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 ocorre no thread da interface do usuário. Isso significa que você deve certificar-se de retornar periodicamente ao Dispatcher para processar eventos de entrada pendentes antes que eles fiquem inúteis.

Um aplicativo de exemplo que demonstra os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic.

Considere o seguinte exemplo:

Captura de tela que mostra a organização de números primos.

Este aplicativo simples conta a partir de três, buscando números primos. Quando o usuário clica no botão Iniciar , a pesquisa começa. Quando o programa encontra um primo, ele atualiza a interface do usuário informando sobre sua descoberta. A qualquer momento, o usuário pode interromper a pesquisa.

Embora simples o suficiente, a pesquisa de números primos pode continuar para sempre, o que apresenta algumas dificuldades. Se tratarmos toda a pesquisa dentro do manipulador de eventos de clique do botão, nunca daremos ao thread da interface do usuário a chance de processar outros eventos. A interface do usuário não poderá responder a mensagens de entrada ou de processo. Ele nunca repintaria e nunca responderia a cliques no 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 de thread único, podemos atualizar diretamente o rótulo que lista o maior primo encontrado.

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

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

Captura de tela que mostra a fila do dispatcher.

O Microsoft Word realiza 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 em 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 código subjacente.

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 manipulador StartStopButton_Click é responsável por agendar a primeira verificação de um número primo, adicionando um delegado à fila Dispatcher. Algum tempo depois que esse manipulador de eventos concluir seu trabalho, ele Dispatcher selecionará o delegado para execução.

Como mencionamos anteriormente, InvokeAsync é o Dispatcher membro usado para agendar um delegado para execução. Nesse caso, escolhemos a SystemIdle prioridade. Ele Dispatcher executará esse delegado somente quando não houver eventos importantes a serem processados. 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 é primo. Se for primo, o método atualizará diretamente o bigPrimeTextBlock para indicar sua descoberta como número primo. Podemos fazer isso porque o cálculo está ocorrendo no mesmo thread que foi usado para criar o controle. Se tivéssemos escolhido 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 em seguida.

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 verdade se houver alguma chance de que uma das janelas monopolize o thread.

O Windows Explorer funciona dessa forma. Cada nova janela do Explorer pertence ao processo original, mas é criada sob o controle de um thread independente. Quando o Explorer se torna não responsivo, como ao procurar recursos de rede, outras janelas do Explorer continuam sendo responsivas e utilizáveis.

Podemos demonstrar esse conceito com o exemplo a seguir.

Uma captura de tela de uma janela do WPF duplicada quatro vezes. Três das janelas indicam que estão usando o mesmo thread, enquanto as outras duas estão em threads diferentes.

As três principais janelas dessa imagem compartilham o mesmo identificador de thread: 1. As duas outras janelas têm diferentes identificadores de thread: Nove e 4. Há um glifo giratório na 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 no thread atual ou em um novo thread. O ‼️ glifo gira constantemente até que o botão Pausar 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 se tornam não dinâmicas. Qualquer janela em um thread diferente continua funcionando 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 código subjacente.

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

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

    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 desse thread. O WPF cria automaticamente um novo System.Windows.Threading.Dispatcher para gerenciar o novo 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 que demonstra os conceitos desta seção pode ser baixado do GitHub para C# ou Visual Basic.

Gerenciar 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 de bloqueio a partir dos manipuladores de eventos porque o aplicativo parece congelar. O exemplo anterior criou novas janelas em seu próprio thread, permitindo que cada janela seja executada independentemente uma da outra. Embora possamos criar um novo thread com System.Windows.Threading.Dispatcher, torna-se difícil sincronizar o novo thread com o thread principal da interface do usuário após a conclusão do trabalho. Como o novo thread não pode modificar a interface do usuário diretamente, precisamos usar Dispatcher.InvokeAsync, Dispatcher.BeginInvokeou Dispatcher.Invoke, para inserir delegados no Dispatcher thread da interface do usuário. Eventualmente, esses delegados são executados com permissão para modificar elementos da interface de usuário.

Há uma maneira mais fácil de executar o código em um novo thread ao sincronizar os resultados, o padrão assíncrono baseado em tarefa (TAP). Ele é baseado nos tipos Task 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 a iniciação e a conclusão de uma operação assíncrona. Há alguns benefícios para esse 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 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 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 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 é suspenso e não bloqueia o restante do thread da interface do usuário. O contexto de sincronização do WPF manipula a suspensão do código, o que permite que o WPF continue a ser executado.

Um diagrama que demonstra o fluxo de trabalho do aplicativo de exemplo.

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. Após algum tempo, o aplicativo retorna com uma imagem do sol ou de nuvens de chuva, dependendo do resultado dos dados.

Um aplicativo de exemplo que demonstra 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 no XAML. O XAML usa um único botão para buscar a previsão do tempo.

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 de 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 (ou Async com 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 await (ou Await com o Visual Basic). Até que FetchWeatherFromServerAsync seja concluído, o código do manipulador do botão é suspenso e o controle é retornado para quem chamou. 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 o 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 (ou await FetchWeatherFromServerAsync(); com o Visual Basic) faz com que o código seja Await FetchWeatherFromServerAsync() executado no thread da interface do usuário, mas não é executado no dispatcher tem tempo para executá-lo, semelhante à forma como o FetchWeatherFromServerAsync 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. Portanto, FetchWeatherFromServerAsync é executado em sua própria linha de execução.

  • 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 manter as coisas simples, 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. Durante esse período, a linha de execução original da interface do usuário ainda está em execução e respondendo a eventos, enquanto o manipulador de eventos do botão está pausado até que a nova linha de execução seja concluída. 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 terminar Task.Delay , e selecionarmos aleatoriamente nossa previsão do tempo, o 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 de interface do usuário tiver tempo, o chamador do manipulador de Task.Runeventos do botão será retomado. O restante 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 que demonstra 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 multithreading.

Bombeamento aninhado

Às vezes, não é viável bloquear completamente o thread da interface do usuário. Vamos considerar o Show método da MessageBox classe. 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 interativo. Enquanto esperamos que o usuário clique em OK, a janela original do aplicativo não responde à entrada do usuário. No entanto, ele continua a processar mensagens de renderização. A janela original é redesenhada quando coberta e revelada.

Captura de tela que mostra uma MessageBox com um botão OK

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 não seria capaz de pintar os elementos desabilitados na janela original (lembre-se da discussão anterior sobre exclusão mútua). O WPF, em vez disso, 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 é concluído, a execução é retomada após a chamada original PushFrame .

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

Eventos roteados obsoletos

O sistema de eventos roteado 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 a finalização de handler2, o evento é passado para o objeto Canvas, que utiliza handler1 para o processamento. Isso ocorrerá somente se handler2 não marcar explicitamente o objeto de evento como manipulado.

É possível que handler2 leve muito tempo para processar esse evento. handler2 pode usar 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 sendo muito antigo.

Reentrância e bloqueio

O mecanismo de bloqueio do CLR (Common Language Runtime) não se comporta exatamente como se pode 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 deadlocks e tornar as interfaces minimamente responsivas, mas apresenta a possibilidade de bugs sutis. A 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. Nesse caso, esse único segmento pode fazer alterações ambientais em momentos inesperados, causando esses efeitos nocivos que o DispatcherObject mecanismo de exclusão mútua deve ser capaz de resolver. Considere o seguinte pseudocódigo:

Diagrama que mostra a reentrância de encadeamento.

Na maioria das vezes, essa é a coisa certa, mas há momentos no WPF em que essa reentrada inesperada pode realmente causar problemas. Portanto, em determinados momentos-chave, o WPF chama DisableProcessing, o que altera a instrução de bloqueio desse thread para usar o bloqueio livre de reentrada do WPF, em vez do bloqueio CLR habitual.

Por que a equipe CLR escolheu esse comportamento? Ele tinha a ver com objetos COM STA e o thread de finalização. Quando um objeto é coletado pelo coletor de lixo, o seu método Finalize é executado na thread do finalizador dedicada, não na thread da interface do usuário. E aí reside o problema, pois um objeto COM STA que foi criado no thread de interface do usuário só pode ser descartado no thread da interface do usuário. O CLR faz o equivalente a um BeginInvoke (nesse caso, usando o SendMessage do Win32). No entanto, se o thread da interface do usuário estiver ocupado, o thread do finalizador será interrompido e o objeto COM STA não poderá ser descartado, o que criará um grave vazamento de memória. Então a equipe CLR tomou a difícil decisão de fazer os bloqueios funcionarem da forma como funcionam.

A tarefa do WPF é evitar a reentração inesperada sem reintroduzir o vazamento de memória, razão pela qual não bloqueamos a reentrada em todos os lugares.

Consulte também