.NET

20 dez, 2016

VB .NET – usando Tasks

Publicidade

Neste artigo, vou apresentar o conceito de Tasks e mostrar um exemplo básico de sua aplicação usando a linguagem VB .NET.

A plataforma .NET versão 4.0 apresenta o novo namespace System.Threading.Tasks, que contém classes que permitem abstrair a funcionalidade de threading onde, na verdade, por trás dos panos, uma ThreadPool é usada.

Uma tarefa (ou task) representa uma unidade de trabalho que deverá ser realizada. Esta unidade de trabalho pode rodar em uma thread separada e é também possível iniciar uma task de forma sincronizada, que resulta em uma espera pela thread chamada. Com tarefas, você tem uma camada de abstração, mas também um bom controle sobre as threads relacionadas.

As tarefas (tasks) permitem muito mais flexibilidade na organização do trabalho que você precisa fazer. Por exemplo, você pode definir continuar o trabalho, que deve ser feito depois que uma tarefa esteja completa. Isso pode diferenciar se um tarefa foi executada com sucesso ou não.

Você também pode organizar as tarefas em hierarquia, onde uma tarefa pai pode criar novas tarefas filhas, que pode criar dependências e, assim, o cancelamento da tarefa pai também cancela suas tarefas filhas.

Mas por que eu deveria usar Tasks e não o recurso padrão de Multithreading?

  1. Tasks são a nova abordagem de realizar operações assíncronas e multitarefas; as novas palavras chaves ‘async’ e ‘await’ facilitam uso de operações multitarefas com Tasks;
  2. O recurso Tasks foi otimizado para fazer uso do pool de threads do CLR, e por isso não tem a sobrecarga associada à criação de um segmento dedicado usando a classe Thread;
  3. As Tasks implementam filas de trabalho locais. Essa otimização permite criar eficientemente muitas tarefas filhas com execução rápida, sem incorrer em sobrecarga – que de outra forma ocorreria com uma única fila de trabalho;
  4. Você pode esperar a conclusão das tarefas sem o uso de construções de sinalização (classe Monitor etc);
  5. É possível encadear as tarefas para executar uma após a outra.

A seguir, vou apresentar um exemplo que simula uma operação computacional intensa em uma aplicação Windows Forms, em que iremos usar os recursos do namespaces System.Threading.Tasks.

Recursos usados: Visual Studio 2015 Community

Criando o projeto no VS Community 2015

Abra o VS Community 2015 e clique em New Project. Selecione a linguagem Visual Basic e o template Windows Forms Application. Depois informe o nome VBNET_Tasks e clique no botão OK.

No formulário form1.vb, inclua os seguintes controles:

  • 1 TextBox – Name=txtPrimos, Multiline=True
  • 1 Button – btnCalcular
  • 2 NumericUpDown – nudInferior, nudSuperior

Disponha os controles no formulário conforme o leiaute da figura abaixo:

vbn_task11

Esta singela aplicação terá o objetivo de contar números primos. Funciona assim:

  • O usuário informa uma intervalo de números usando os controles NumericUpDown;
  • Clique no botão Calcular;
  • O número de números primos que estiver contido no intervalo informado será exibido na caixa de texto.

A ideia é que, se o usuário entrar um intervalo muito amplo de números, a operação para contar os números primos vai demorar e a interface do usuário (UI) ficará congelada até que a operação termine. Isso acontecerá a menos que você, como programador que conhece Tasks, previna esse comportamento.

A abordagem tradicional em que a interface do usuário congela

Então, inclua o código abaixo no formulário Form1.vb:

Imports System.Threading.Thread
Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        'atribui os valores máximos para o intervalo númerico
        Me.nudSuperior.Maximum = Integer.MaxValue
        Me.nudInferior.Maximum = Integer.MaxValue
    End Sub
    Private Function getPrimosNoIntervalo(valorInicialInclusivo As Integer, valorFinalExclusivo As Integer) As IEnumerable(Of Integer)
        'força um atraso de 2 segundos
        Sleep(TimeSpan.FromMilliseconds(2000))
        'retorna quantos números primos existe no intervalo
        Return getTodosNumerosPrimos(Enumerable.Range(valorInicialInclusivo, (valorFinalExclusivo - valorInicialInclusivo)))
    End Function
    Private Function getTodosNumerosPrimos(numeros As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
        'calcula a quantidade de número primos 
        Return From n In numeros Where (Not (n <= 1) AndAlso Enumerable.Range(2, CInt(Math.Sqrt(If(n = 2, 0, n)))).All(Function(i) n Mod i > 0)) Select n
    End Function
    Private Sub btnCalcular_Click(sender As Object, e As EventArgs) Handles btnCalcular.Click
        'exibe a quantidade de número primos
        Me.txtPrimos.Text += Me.getPrimosNoIntervalo(Integer.Parse(Me.nudInferior.Value.ToString()), Integer.Parse(Me.nudSuperior.Value.ToString())).Count().ToString() + Environment.NewLine
    End Sub
    Private Sub nudInferior_Validating(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles nudInferior.Validating
        'valida a entrada do usuário
        If Me.nudInferior.Value > Me.nudSuperior.Value Then
            Me.nudInferior.Value = Me.nudSuperior.Value
        End If
    End Sub
End Class

Se você executar o projeto e informar um intervalo de 0 a 200, por exemplo, e clicar no botão Calcular, vai notar que a interface do usuário vai ‘congelar’ e não vai permitir nenhuma nova interação do usuário enquanto estiver realizando os cálculos.

Queremos evitar esse problema permitindo que o usuário informe um intervalo de números, clique no botão Calcular e, a seguir, possa inserir um novo intervalo enquanto os cálculos estão sendo processados.

Vejamos, então, como fazer isso usando os recursos do namespace System.Threading.Tasks.

A abordagem usando Tasks

Inclua um novo Button no formulário form1.vb com o nome btnCalcularTasks, conforme mostra a figura abaixo:

vbn_task12

Para começar, vamos incluir no formulário a declaração do namespace:

Imports System.Threading.Tasks

Agora no evento Click do botão Calcular com Tasks, inclua o código abaixo:

Private Sub btnCalcularTasks_Click(sender As Object, e As EventArgs) Handles btnCalcularTasks.Click
        'para evitar o acesso cross thread dos controles vamos armazenar os valores em variáveis
        Dim inferior As Integer = Integer.Parse(Me.nudInferior.Value.ToString())
        Dim superior As Integer = Integer.Parse(Me.nudSuperior.Value.ToString())
        'inicia uma nova task, e então exibe o resultado no textbox
        Task.Factory.StartNew(Of Integer)(Function() Me.getPrimosNoIntervalo(inferior, superior).Count())
    End Sub

Vamos entender o código:

Envolvemos a chamada para o método getPrimosNoIntervalo() em uma task. Dessa forma, agora teremos uma execução assíncrona lado a lado com a principal Thread de interface. Com isso, a interface do usuário não vai congelar e vai continuar responsiva durante a operação de cálculo.

Neste código, estamos usando uma versão genérica do método StartNew() e isso significa que queremos que a task retorne um valor (no exemplo, a quantidade de números primos). Se eu não desejasse retornar um valor, usaria a versão não genérica do método.

Bem, estamos realizando o cálculo mas ainda não estamos exibindo o resultado no TextBox.

É neste ponto que teríamos de usar Invoke() ou BeginInvoke() para fazer uma chamada para o segmento de interface do usuário, para permitir atualizar a caixa de texto se estivéssemos usando uma thread dedicada.

Porém, usando Tasks a tarefa fica mais fácil.

Primeiro vamos incluir a seguinte linha de código no início:

Private scheduler As TaskScheduler = Nothing

A seguir, no construtor do formulário form1.vb, inclua a linha de código abaixo:

Me.scheduler = TaskScheduler.FromCurrentSynchronizationContext()

Este código atribui ao campo uma tarefa agendada para a thread da interface do usuário e, assim, podemos usá-la para atualizar a caixa de texto.

A seguir, precisamos atualizar a caixa de texto com o valor retornado da task de cálculo. Para fazer isso usamos o método ContinueWith() que permite encadear as tarefas juntas, uma depois da outra.

Usando a classe Tasks, você pode especificar que, depois que uma tarefa for concluída, outra tarefa específica deve começar a ser executada. Por exemplo, uma nova tarefa que usa um resultado da anterior ou que deve fazer uma limpeza se a tarefa anterior falhou.

Vamos alterar o código do evento Click do botão Calcular com Tasks conforme abaixo:

Private Sub btnCalcularTasks_Click(sender As Object, e As EventArgs) Handles btnCalcularTasks.Click
        'para evitar o acesso cross thread dos controles vamos armazenar os valores em variáveis
        Dim inferior As Integer = Integer.Parse(Me.nudInferior.Value.ToString())
        Dim superior As Integer = Integer.Parse(Me.nudSuperior.Value.ToString())
        'inicia uma nova task, e então exibe o resultado no textbox
        Task.Factory.StartNew(Of Integer)(Function() Me.getPrimosNoIntervalo(inferior, superior).Count()).
                 ContinueWith(Function(i) Me.txtPrimos.Text = Me.txtPrimos.Text + i.Result.ToString() + Environment.NewLine, Me.scheduler)
    End Sub

Agora, podemos testar o projeto usando intervalos maiores ou menores sem que a interface do usuário fique congelada.

Pegue o projeto completo aqui: VBNET_Tasks.zip