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?
- 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;
- 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;
- 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;
- Você pode esperar a conclusão das tarefas sem o uso de construções de sinalização (classe Monitor etc);
- É 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:
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:
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