Compartilhar via


Processamento paralelo e assíncrono no .NET Framework

O objetivo deste artigo é descrever como tarefas podem ser processadas de forma paralela e assincronamente no .NET Framework, empregando para isto recursos disponibilizados pelas classes Parallel e Task.

Introdução

Soluções como jogos e aplicativos para dispositivos móveis constituem bons exemplos de projetos que priorizam aspectos como o paralelismo e a execução assíncrona de tarefas. Além de enfatizarem uma resposta rápida às ações dos usuários, tais aplicações também devem ser modeladas para que se possibilite a sua utilização simultânea com outros softwares.

O conceito de paralelismo diz respeito à execução em um sistema de diferentes tarefas ao mesmo tempo, visando assim o melhor aproveitamento dos recursos disponibilizados. Já a noção de processamento assíncrono se refere à capacidade de uma aplicação em continuar respondendo de maneira aceitável, por mais que um conjunto de atividades iniciadas pela mesma ainda esteja em curso.

No caso específico da plataforma .NET, o suporte ao paralelismo acontece através da classe Parallel. Quanto à implementação de um mecanismo de processamento assíncrono, será o tipo Task que fornecerá os recursos necessários para tal comportamento. Importante destacar ainda que estas duas classes são parte do namespace System.Threading.Tasks, integrando o .NET Framework desde a versão 4.0.

Na próxima seção será construída uma aplicação WPF que faz uso dos tipos Parallel e Task, a fim de demonstrar a aplicação dos conceitos aqui descritos.

Implementando o processamento paralelo e assíncrono

Para implementar a aplicação de testes demonstrada nesta seção foram utilizados os seguintes recursos:

  • O Microsoft Visual Studio Professional 2013 Update 4 como IDE de desenvolvimento;
  • O .NET Framework 4.5.1;
  • O framework Windows Presentation Foundation (WPF) para a construção de uma interface gráfica.

O projeto TesteParallelAsync será do tipo “WPF Application”, como indicado a seguir:

Na próxima figura é possível observar o formulário MainWindows devidamente configurado, com os controles que servirão de base para a implementação das funcionalidades da aplicação de testes. Esta tela será construída de forma a possibilitar o download simultâneo de vários arquivos, sem que isto implique em falta de responsividade na interface gráfica (como travamentos enquanto os arquivos estão sendo baixados).

Por questões de simplificação, não serão abordados neste artigo maiores detalhes sobre como montar uma interface gráfica em WPF. A solução aqui descrita foi disponibilizada no Technet Gallery, podendo ser baixada através do link:

https://gallery.technet.microsoft.com/Processamento-paralelo-e-93a079b6

Na listagem detalhada a seguir estão alguns dos eventos que serão implementados para este projeto de testes (mais especificamente no formulário MainWindow). Constam neste bloco de código as seguintes contruções:

  • Atributo _podeEncerrarAplicacao: flag que indica se o formulário MainWindow pode ou não ser fechado num determinado instante. A ideia por trás disto é impedir o encerramento da aplicação caso existam downloads em curso;
  • Evento btnAdicionarUrlDownload_Click: acionado ao se clicar no botão para a inclusão de um endereço para download (com a URL em questão sendo preenchida através de um TextBox criado para este fim). No início deste método são realizadas algumas validações, a fim de determinar se a URL informada é válida (com a emissão de um alerta se forem detectados problemas). Caso se trate realmente de um endereço ainda não adicionado, o valor será incluído em um ListBox para dowload posteriormente;
  • Evento btnLimparListaUrlsParaDownload_Click: disparado por meio do botão que permite limpar a lista de URLs para download;
  • Evento Window_Closing: verificação realizada antes de se fechar a interface gráfica, com o intuito de finalizar a aplicação. Caso downloads estejam em progresso, a instrução contida neste método impedirá o encerramento do executável de testes;
  • Método GetNomeArquivo: função auxiliar utilizada para se obter o nome de um arquivo indicado em uma URL (empregando para isto de recursos das classes Path e Uri).

Ainda sobre as validações efetuadas a partir do evento btnAdicionarUrlDownload_Click, é possível notar:

  • O uso do método IsNullOrWhiteSpace da classe String, com o objetivo de verificar o preenchimento da URL e se a mesma representa um arquivo disponível para download (neste último caso, em conjunto com a função GetNomeArquivo);
  • Uma chamada ao método IsWellFormedUriString da classe Uri (namespace System.Net), de forma a determinar se realmente foi informada uma URL válida.
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Net;
using System.IO;
 
namespace TesteParallelAsync
{
    public partial  class MainWindow : Window
    {
 
        ...
 
        private bool  _podeEncerrarAplicacao = true;
 
        private void  btnAdicionarUrlDownload_Click(object sender, RoutedEventArgs e)
        {
            string url = txtUrlDownload.Text;
            if (!String.IsNullOrWhiteSpace(url) &&
                Uri.IsWellFormedUriString(url, UriKind.Absolute) &&
                !String.IsNullOrWhiteSpace(GetNomeArquivo(url)))
            {
 
                if (!lstUrlsParaDownload.Items.Contains(url))
                {
                    lstUrlsParaDownload.Items.Add(url);
                    txtUrlDownload.Clear();
                }
                else
                    MessageBox.Show("URL já incluída na lista para download!");
            }
            else
                MessageBox.Show("URL inválida!");
 
            txtUrlDownload.Focus();
        }
 
        private void  btnLimparListaUrlsParaDownload_Click(object sender, RoutedEventArgs e)
        {
            lstUrlsParaDownload.Items.Clear();
        }
 
        private void  Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            e.Cancel = !this._podeEncerrarAplicacao;
        }
 
        private string  GetNomeArquivo(string url)
        {
            return Path.GetFileName(new Uri(url).AbsolutePath);
        }
 
        ...
 
    }
}

Já na próxima listagem está a definição do método btnEfetuarDownload_Click, o qual corresponde à implementação do evento responsável pelo download de múltiplos arquivos. Quanto à forma como este método foi codificado, é importante destacar:

  • O uso da palavra-chave async na assinatura deste evento. Isto é uma indicação de que a operação será executada de forma assíncrona. Métodos com este tipo de comportamento são executados sem que se bloqueie a execução da aplicação, por mais que a tarefa em questão implique em um processamento mais lento;
  • Instruções verificando se foram selecionadas URLs para download;
  • As diferentes funcionalidades da tela de testes estão sendo desabilitadas durante o download (por meio de uma chamada ao método HabilitarFuncionalidades). Embora a aplicação ainda permaneça responsiva, a ideia por trás desta instrução é impedir que um usuário inicie uma nova leva de dowloads (enquanto arquivos estiverem sendo baixados);
  • Caso ainda não exista, uma pasta chamada “Downloads” será criada no diretório em que se encontra a aplicação (este caminho é obtido através do método GetDiretorioDestino);
  • As diferentes URLs selecionadas no ListBox lstUrlsParaDownload serão convertidas em um array. Estes endereços serão repassados como parâmetro ao método ForEach da classe Parallel, a fim de possibilitar a execução paralela da operação EfetuarDownloadArquivo. Caberá a este último método efetuar o download de cada arquivo representado por uma URL. Todo este processo acontecerá de forma assíncrona, através da invocação do método Run da classe Task (por este motivo, tal instrução foi precedida pela palavra-chave await);
  • Quando o processo de download se encerrar aparecerá uma mensagem informativa, com as funcionalidades da tela sendo habilitadas novamente (via método HabilitarFuncionalidades).


      private async void btnEfetuarDownload_Click(object sender, RoutedEventArgs e)
      {  
                    try      
                    {      
                        if (lstUrlsParaDownload.Items.Count == 0)  
                        {      
                            MessageBox.Show(      "Não foram definidas URLs para download!"      );      
                            return      ;      
                        }      
   
                        HabilitarFuncionalidades(      false      );      
                        lstProgressoDownload.Items.Clear();      
   
                        string diretorioDestino = GetDiretorioDestino();  
                        if (!Directory.Exists(diretorioDestino))  
                            Directory.CreateDirectory(diretorioDestino);      
   
                        var urls =      
                            lstUrlsParaDownload.Items.OfType<      string      >().ToArray();      
                        lstUrlsParaDownload.Items.Clear();      
   
                        await Task.Run(() => Parallel.ForEach(      
                            urls, url => EfetuarDownloadArquivo(url)));      
   
                        MessageBox.Show(      "Download(s) concluído(s)!"      );      
                    }      
                    finally      
                    {      
                        HabilitarFuncionalidades(      true      );      
                    }      
      }  

Por fim, na listagem apresentada a seguir encontra-se a implementação de alguns dos métodos utilizados pelo evento btnEfetuarDownload_Click:

  • Método HabilitarFuncionalidades: responsável por ativar ou não as diferentes funcionalidades da tela de testes (inclusão de URL para download, processamento do dowload, fechamento da tela etc.);
  • Método GetDiretorioDestino: função que retorna o caminho completo para gravação de arquivos baixados, fazendo uso para isto da classe AppDomain (namespace System);
  • Método EfetuarDownloadArquivo: operação criada para o download de arquivos, empregando para isto as técnicas de processamento paralelo e assíncrono descritas anteriormente. Será através do método DownloadFile da classe WebClient (namespace System.Net) que um arquivo será baixado. As chamadas ao método ExibirProgressoDownload permitem ainda informar ao usuário do progresso de uma tarefa deste tipo (início e término);
  • Método ExibirProgressoDownload: função responsável por registrar uma mensagem informando o progresso de um download, utilizando para tanto o ListBox lstProgressoDownload (através de uma chamada ao método Invoke, a partir do objeto Dispatcher associado a este controle).
private void  HabilitarFuncionalidades(bool habilitar)
{
    this._podeEncerrarAplicacao = habilitar;
 
    txtUrlDownload.IsEnabled = habilitar;
    btnAdicionarUrlDownload.IsEnabled = habilitar;
    btnLimparListaUrlsParaDownload.IsEnabled = habilitar;
    btnEfetuarDownload.IsEnabled = habilitar;
}
 
private string  GetDiretorioDestino()
{
    return AppDomain.CurrentDomain.BaseDirectory + "Downloads\\";
}
 
private void  EfetuarDownloadArquivo(string urlDownload)
{
    ExibirProgressoDownload(urlDownload, "Início do download");
 
    WebClient client = new  WebClient();
    client.DownloadFile(urlDownload,
        GetDiretorioDestino() + GetNomeArquivo(urlDownload));
 
    ExibirProgressoDownload(urlDownload, "Término do download");
}
 
private void  ExibirProgressoDownload(string urlDownload, string acao)
{
    lstProgressoDownload.Dispatcher.Invoke(
        new Action(() =>
        {
            lstProgressoDownload.Items.Add(String.Format(
                "{0} - {1} - {2}",
                DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"),
                acao, urlDownload));
        }), null);
}

Testes

Na imagem seguinte está a tela inicial da aplicação TesteParallelAsync:

Para efeitos de testes, serão incluídas algumas URLs de arquivos para download:

Acionando na sequência o botão “Download”, será iniciado um processo com o objetivo de baixar os arquivos correspondentes às URLs informadas anteriormente:

No decorrer deste procedimento, será possível notar que a tela para testes permanece responsiva (embora com as suas funcionalidades desativadas). Tão logo esta atividade se encerre, aparecerá a indicação do término dos downloadas (além de um alerta confirmando o sucesso desta operação):

Conforme indicado na próxima imagem, uma pasta chamada “Downloads” foi criada dentro da pasta da aplicação. Os arquivos baixados através do método EfetuarDownloadArquivo já estarão inclusive dentro deste subdiretório:

Conclusão

Como demonstrado na parte prática deste artigo, as classes Task e Parallel são hoje a alternativa mais simples para a implementação de rotinas capazes de processar instruções de forma paralela e assincronamente em aplicações .NET. Dispensando o desenvolvedor da necessidade de implementar extensos e intricados blocos de código, estes tipos dispõem de inúmeras funcionalidades que visam facilitar a construção de aplicações com uma maior responsividade.

Referências

Asynchronous Programming with Async and Await (C# and Visual Basic)
http://msdn.microsoft.com/en-us/library/hh191443.aspx

Parallel Class
http://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel(v=vs.110).aspx

Task Class
http://msdn.microsoft.com/en-us/library/system.threading.tasks.task%28v=vs.110%29.aspx

Task Parallelism (Task Parallel Library)
http://msdn.microsoft.com/en-us/library/dd537609(v=vs.110).aspx