Threading em WPF


Quando desenvolvemos aplicações Windows, é muito comum em algum ponto da mesma, que algumas tarefas mais complicadas e custosas sejam realizadas, que podem levar um tempo maior até que seja concluída. Independentemente do que ela faça, seja um cálculo, uma consulta em uma base de dados ou uma chamada para um serviço, se você executar esse código de forma síncrona, o usuário deverá esperar até que essa tarefa seja finalizada, para a partir daí, conseguir acessar outras áreas do sistema.

Ao rodar uma aplicação Windows, um processo é criado dentro do sistema operacional. Processo não executa nenhum código; são as threads que fazem isso. Quando o processo é criado, uma thread é criada juntamente com ele, e esta é muitas vezes chamada de main-thread (thread principal). Essa thread nasce e morre com o término do processo, ou seja, enquanto ela estiver executando alguma tarefa, o processo continuará ativo.

As aplicações Windows que conhecemos, como Windows Forms, Console, Windows Services e WPF trabalham nesta mesma linha. Aplicações que possuem gráficos, como é o caso do Windows Forms e do WPF, tem um agravante: a afinidade que os controles tem com a thread principal. Quando a aplicação é iniciada, a thread principal é quem cria os controles (Form, TextBox, Label, TextBlock, etc.), e quando dizemos que há uma afinidade, isso quer dizer que podemos somente manipular esses controles, através da mesma thread que os criaram, e qualquer tentativa de fazer isso através de uma segunda thread, uma exceção do tipo InvalidOperationException será disparada.

Quando essas tarefas são finalizadas, é normal queremos exibir o resultado para o usuário, que na maioria das vezes, implica em alterar a propriedade Text de algum controle, exibir uma MessageBox, etc. Em Windows Forms, todos os controles herdam direta ou indiretamente da classe Control, que fornece um método chamado Invoke, e que dado um delegate, executa o método relacionado na mesma thread do criador. Mais tarde, com o .NET Framework 2.0, surgiu o SynchronizationContext, que facilitou bastante a atualização dos controles a partir de uma thread secundária.

Agora temos o WPF, que traz novas funcionalidades e uma forma um pouco diferente para lidar com esse tipo de problema. Vamos a partir deste artigo, explorar um pouco mais sobre o modelo de threading do WPF. A Microsoft introduziu uma série de novos tipos, espalhados por vários namespaces e que serão utilizados para conseguir atingir o nosso principal objetivo. Para iniciar, o primeiro tipo que temos que conhecer é a classe Dispatcher. Essa classe serve como um gerenciador de tarefas para serem executadas, e está sempre associada com uma determinada thread de UI. Ela mantém uma fila de tarefas que são executadas utilizando a thread qual está relacionada.

A fila que é mantida pela classe Dispatcher é priorizada, que permite especificar uma prioridade antes de enfileirar a tarefa (mais detalhes abaixo). Para alistar uma tarefa nesta fila, você poderá utilizar o método Invoke ou BeginInvoke. A diferença entre eles é clara: o primeiro executa a tarefa de forma síncrona, enquanto a segunda alista a tarefa para ser executada de forma assíncrona. E para sedimentar, ambas sempre executarão na thread ao qual o Dispatcher está vinculado.

Grande parte das classes que compõem o framework do WPF, incluindo os controles, herda direta ou indiretamente da classe abstrata DispatcherObject, que possui uma estrutura simples, ou seja, fornece uma propriedade chamada Dispatcher que retorna a instância de uma classe Dispatcher, e como já era de esperar, fornece a instância do Dispatcher que está vinculado com aquele classe/controle.

A classe DispatcherObject ainda fornece dois métodos importantes: CheckAccess e VerifyAccess. A diferença entre eles é que o primeiro retorna um valor boleano, indicando se a thread que está chamando tem direito de acesso ao Dispatcher correspondente. Já o segundo método, VerifyAccess, dispara uma exceção do tipo InvalidOperationException, caso a thread que está chamando não tiver direito de acesso ao Dispatcher. Como pudemos perceber, esses métodos vão nos auxiliar para determinar se há ou não a necessidade de atualizar o controle através da thread atual, sem que seja necessário utilizar o Dispatcher para chegar até o controle.

Para exemplificar, imagine que temos uma thread que executará algum cálculo complexo, e depois de calculado, deverá exibir o resultado em um TextBox. Como comentado acima, dentro desta thread não podemos alterar qualquer propriedade do TextBox, e para solucionar isso no WPF, vamos recorrer a propriedade Dispatcher do TextBox (“txt”), que foi herdada de DispatcherObject. Ao passar um delegate para o método Invoke, ele será executado (de forma síncrona) na mesma thread que criou o controle.

new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    txt.Dispatcher.Invoke(new Action<int>(r => txt.Text = r.ToString()), result);
}).Start();

Se desejar, podemos trocar o método Invoke por BeginInvoke, e a atualização do controle será feita em background. A vantagem desta técnica é que você pode executar a atualização do controle enquanto faz outras tarefas. É importante que você mantenha tarefas “leves” dentro do Dispatcher, pois tudo o que ele deveria fazer ali é atualizar a UI; colocar tarefas mais complexas, voltará a ter concorrência com os eventos dos controles e, consequentemente, o usuário voltará a ter os travamentos das telas do sistema, que acontecia quando trabalhávamos de forma síncrona.

Quando você opta por utilizar o método BeginInvoke, ele retorna uma instância da classe DispatcherOperation. Basicamente, este objeto representa uma espécie de “ponteiro” para a operação que está sendo executada. Essa classe fornece uma série de membros interessantes, e entre eles temos:

  • Dispatcher: O Dispatcher relacionado.
  • Priority: Uma das opções definidas no enumerador DispatcherPriority, que define a prioridade da operação.
  • Result: Retorna um System.Object com o resultado da tarefa (isso quando ela retornar algum resultado).
  • Status: Uma das opções definidas no enumerador DispatcherOperationStatus, que define o status atual da operação (Pending, Aborted, Completed ou Execution).
  • Abort: Método que aborta a operação que está sendo executada.
  • Wait: Quando invocado, fará um “join” na thread atual, aguardando até o término da operação. Opcionalmente você pode especificar um timeout.
  • Aborted: Evento que é disparado quando a operação é abortada.
  • Completed: Evento que é disparado quando a operação foi finalizada.

Com a instância do DispatcherOperation em mãos, podemos utilizar duas formas para chegar até o resultado, que é via eventos ou através de polling. Utilizando o modelo de eventos, podemos nos vincular ao evento Completed, e quando a operação for finalizada, esse evento será automaticamente disparado. Já o polling consiste em testar, de tempo em tempo, se a operação finalizou ou não. Abaixo temos os dois exemplos de utilização:

new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    DispatcherOperation op =
        txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), result);

    op.Completed += (o, args) => MessageBox.Show(“Finalizou Tudo”);
}).Start();

new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    DispatcherOperation op =
        txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), result);

    //Faz Algo…

    while (op.Status != DispatcherOperationStatus.Completed)
    {
        if (op.Wait(TimeSpan.FromSeconds(5)) == DispatcherOperationStatus.Completed)
        {
            //Finalizou a Atualização da UI
            break;
        }
    }
}).Start();

Tanto o método Invoke quanto o BeginInvoke possui versões (overloads) destes métodos que permitem especificar uma prioridade, e para isso, utilizamos uma das doze opções definidas pelo enumerador DispatcherPriority. Há algumas opções interessantes, como por exemplo Inactive, que permite você alistar a operação, mas que não será processada. De qualquer forma, na maioria das vezes a opção Normal (que também é o padrão), já será o suficiente. Abaixo um exemplo de como podemos proceder para especificar a prioridade de uma operação:

txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), DispatcherPriority.Normal, result);

Uma vez que você tem operações alistadas no Dispatcher, o runtime irá determinar quando executá-las, dependendo da prioridade definida. A classe Dispatcher também fornece métodos para abortar todas as operações pendentes de processamento. Para isso, recorremos aos métodos InvokeShutdown ou BeginInvokeShutdown (a diferença entre eles já é sabida). A classe Dispatcher ainda fornece um evento chamado ShutdownFinished, que é disparado quando shutdown do Dispatcher for completamente finalizado, e com isso, você poderá tomar alguma decisão.

Conclusão: Como vimos, ambas tecnologias (Windows Forms e WPF) possuem os mesmos comportamentos. Como trabalhar com ambientes multi-threading não é uma tarefa fácil, a Microsoft introduziu no WPF uma forma diferente de se trabalhar para a atualizar de UI através de uma segunda thread. Além de um modelo suavemente diferente, há algumas melhorias internas que garantem que isso acabe sendo executado de uma forma melhor. E para finalizar, essa técnica visa sempre tornar a aplicação mais amigável para o usuário, sem que eles tenha experiências ruins.

Anúncios

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s