Programação Assíncrona com C#

Na aplicação de exemplo deste artigo uma das principais classes para execução assíncrona de tarefas – System.ComponentModel.BackgroundWorker – é usado para criar um conjunto de tarefas sendo executadas independentemente.

Artigo do tipo Tutorial
Recursos especiais neste artigo:
Conteúdo sobre boas práticas.
Programação Assíncrona com C# - Parte 1
É lugar comum no meio de desenvolvimento de software dizer que enquanto houve um progresso muito grande no hardware os programas não vêm conseguindo aproveitar todo este progresso.

Alguns dizem que o hardware é sub utilizado e que os programas são até mesmo mal feitos, a ponto de não usarem todos os recursos de processamento e memória colocados à disposição.

Discussões à parte, um fato é que realmente os softwares estão subutilizando o hardware existente que também é bem potente. Por outro lado, aproveitar todo este potencial é muito complexo.

A “evolução” dos computadores atuais se deu mais em aumentar o número de processadores disponíveis num mesmo dispositivo do que aumentar o número de operações por segundo que são executadas. Resumindo: se tem mais processadores para executar os programas, mas, a frequência de velocidade destes aumentou pouco se é que aumentou.

Para utilizar efetivamente este potencial é necessário aproveitar a existência de vários processadores e criar rotinas paralelas, que sejam executadas juntamente com outras e que o sistema operacional, ao delegar que se tratam de rotinas paralelas faça a distribuição destas no processador.

Sistemas operacionais mais modernos – Windows 7 e acima, Mac OS e o Linux – lidam bem com paralelismo e distribuição das tarefas nos processadores, mas, boa parte dos programas só agora começa a ser preparada para executar eficientemente rotinas paralelas.

O Framework .NET permite criar estas rotinas e controla-las de uma maneira mais fácil, não que será tão fácil como a programação tradicional, mas, com um pouco de trabalho e organização se consegue bons resultados.

Para melhores resultados é preciso entender diferenças sutis entre rotinas paralelas e assíncronas. A primeira modalidade diz respeito em tarefas sendo executadas de forma simultânea podendo os seus resultados serem ou não esperados ao mesmo tempo. Já tarefas assíncronas são sempre executadas em paralelo, mas não é necessário aguardar a sua finalização, ou, o término de uma rotina não depende do término de outra.

Como é de se esperar o maior problema a ser resolvido é controlar o comportamento de cada tarefa sendo executada assincronamente. Assim, recursos do Framework .NET vão fazer uma boa diferença dando ferramentas bem flexíveis e mais simples para compreensão.

Na aplicação de exemplo deste artigo uma das principais classes para execução assíncrona de tarefas – System.ComponentModel.BackgroundWorker – é usado para criar um conjunto de tarefas sendo executadas independentemente sendo que cada uma possui seus eventos e pode ser interrompida sem afetar a execução da outra.

Em que situação o tema é útil
As técnicas para controlar rotinas paralelas e assíncronas – sim, existem diferenças – apresentadas no artigo ajudarão aos desenvolvedores a iniciar a utilização dos recursos do Framework .NET e da linguagem C# voltados à execução de várias rotinas aproveitando assim os recursos de processamento dos computadores modernos sem perder o controle, possibilitando ao usuário além de iniciar várias rotinas paralelas, saber como está o seu andamento e também cancelar a rotina quando achar necessário.

Hardware e paralelismo

Vamos começar de um jeito diferente. Se puder (e estiver usando o Windows no seu computador, claro), abra o gerenciador de tarefas. Vá para a aba Desempenho e dê uma conferida como está o trabalho dos processadores do seu PC (ou notebook, ou, seja lá o que estiver usando...). Se você for um cara sortudo vai ter quatro ou até oito núcleos de processamento. Se for uma pessoa mais modesta, terá seus meros dois processadores, o que já é um bom começo. Guarde estes dados que já falarei novamente sobre estes.

Há pouco mais de dez anos, o máximo que um computador pessoal poderia ter era um processador com um núcleo simples e quando muito, coprocessadores aritméticos que, segundo especificações técnicas daquela época, eram usados para auxiliar no cálculo de tarefas matemáticas mais complexas. O problema é que nunca ficou muito claro para desenvolvedores de aplicações comerciais quando e como estes processadores auxiliares podiam ser usados. Desde os tempos do Delphi eu nunca cheguei a ver uma rotina que fosse feita para rodar no processador aritmético ou algo assim e realmente não pude pesquisar sobre este assunto.

Mas, segundo a lei te Moore, os processadores dobrariam de capacidade a cada dezoito meses, e, então, alcançariam um limite físico para o número de operações que podem ser feitas por segundo. Bem, parece que estamos perto deste limite. Há um bom tempo a velocidade dos processadores não tem grandes saltos em desempenho e computadores mais comuns, dificilmente ultrapassam os 2.8 GHZ de frequência. O maior problema nem é tanto aumentar a velocidade e sim, dissipar o calor produzido pelo processador. Problemas com hardware e limitações físicas à parte, estas limitações vem sendo muito bem resolvidas através da montagem de processadores em paralelo gerando os processadores multicore que temos e que você deve ter percebido ao usar o gerenciador de tarefas.

Assim, resolve-se o problema de velocidade aumentando o número de processadores. Em teoria se com um processador com a velocidade de 2.2 GHZ se tem um número determinado de tarefas executadas por segundo, ao termos dois processadores da mesma frequência se terá o dobro desta velocidade. Em teoria, mas não é o que ocorre na prática.

Acontece que aumentando o número de núcleos de processamento aumenta-se a complexidade em coordenar o seu funcionamento. É semelhante à gestão de projetos. Se você tem um projeto que precisa ser entregue em três semanas e tem quatro pessoas, se dobrar o número de pessoas não significa necessariamente que o projeto vai ser entregue na metade do tempo. Isto porque você vai precisar coordenar esta equipe e distribuir as tarefas para que uma pessoa da equipe não fique esperando outra terminar uma tarefa da qual a sua depende terminar o trabalho para começar o dela.

Algo análogo a isto ocorre nas CPUs multiprocessadas. É preciso coordenar para que os processadores não fiquem ociosos esperando outro terminar uma tarefa da qual a sua depende. Isto é bem complicado e por causa disto, muitos softwares são feitos quase todos sequencialmente – isto é sem tarefas paralelas – ou, quando muito, existem rotinas paralelas que são mal gerenciadas e acabam causando congelamento da aplicação que fica aguardando uma tarefa terminar para poder continuar com seu trabalho.

Por causa disto ainda há alguma frustração entre os usuários de programas, principalmente aplicações corporativas e comerciais, que muitas vezes investem em hardware, mas quando colocam seus programas para serem executados nestas máquinas não tem o desempenho esperado.

Parte deste problema ocorre principalmente porque o software não é criado para aproveitar os recursos de multiprocessamento. Por outro lado, é muito comum que nestas máquinas multicore, a frequência dos processadores fique muito baixa, em torno dos 1.8 GHZ.

Como solução para isto os desenvolvedores precisam aprender a domesticar as rotinas paralelas. Existe uma brincadeira na minha equipe de trabalho que diz que “ninguém controla uma thread selvagem”.

A boa notícia com o Framework .NET isto pode ser feito mais facilmente. É possível começar bem simples, executando processamento paralelo até criar um verdadeiro thread pool e controlar múltiplas tarefas sendo executadas paralelamente sem permitir que uma interfira na outra.

Thread pool é o termo usado para uma das formas de se controlar várias threads dentro de um programa estabelecendo um ID para esta e podendo interferir no seu funcionamento. Como controle pode se citar a sua interrupção, pausa e também monitoramento do progresso do seu andamento. Uma das maneiras que isto pode ser implementado em C# e no Framework .NET é através do uso de collections. Na aplicação de exemplo é criado um controle análogo a este.

Paralelismo x Rotinas Assíncronas

Rotinas paralelas são executadas em segundo plano, permitindo que outras tarefas sejam executadas ao mesmo tempo. Sua utilização mais comum é quando o software deve fazer uma operação longa como uma consulta no sistema de arquivos ou em um banco de dados, um cálculo mais complexo, uma renderização de imagens, resumindo, qualquer operação que vai levar algum tempo e que se deseja que o programa continue respondendo aos comandos do usuário.

Geralmente são executadas enquanto a interface com o usuário mostra o andamento da tarefa para o usuário que pode ter ou não a opção de cancelar a operação. Existe outra forma de usar tarefas paralelas que é distribuindo seu trabalho em sub-rotinas sendo que cada uma faz uma parte da tarefa principal. Isto já é mais difícil de fazer porque neste caso, mesmo que o trabalho seja executado mais rápido, deve se ter um bom planejamento para fazer a separação das partes e, pode ser que em alguns casos, uma tarefa deva esperar o término de outra para continuar seu trabalho. Neste caso, mesmo executando as tarefas de forma paralela o processamento é síncrono que significa que uma tarefa precisa esperar o término de outra para ser executado.

Quando se fala em rotinas assíncronas está se tratando de uma forma de usar o processamento paralelo onde não é necessário aguardar o término de uma rotina para executar outra. Este tipo de rotina é usada quando não é necessário aguardar o seu término ou quando o resultado final não é necessário para que se possa continuar usando o software. Exemplos bem práticos de rotinas assíncronas são o envio de e-mail, um envio de solicitação de processamento para um banco de dados (como uma consulta, um relatório, um cálculo que possa ser executado enquanto o usuário esteja fazendo outras tarefas).

Ao desenvolver rotinas assíncronas o programador precisa ter um controle do seu andamento, meios para seu cancelamento e alguma forma de conhecer quando esta foi concluída para poder mostrar o resultado para o usuário ou fazer algo com o valor que foi devolvido. Sempre que possível deve se optar por este tipo de rotina, principalmente quando se deseja criar programas que precisam estar constantemente disponíveis para o usuário e não congelar a qualquer custo.

Recursos do Framework .NET

Para poder desenvolver rotinas assíncronas que atendam aos requisitos colocados antes: controle do andamento, término e cancelamento, os desenvolvedores que usam a plataforma .NET contam com vários recursos. O principal é a classe System.Threading.Thread que é usada para fazer com que o código seja executado de forma paralela. Esta é a classe base para o processamento em paralelo e o seu funcionamento bem simples. Considere um método qualquer, como o presente na Listagem 1.

Listagem 1. Estrutura de um método public void MeuMetodo() { // faz alguma coisa... var obj = "Teste"; Console.WriteLine(obj); // Faz mais alguma coisa... }

Os detalhes da implementação não são importantes por isso foram omitidos, considere que este método faça qualquer tipo de processamento. No corpo do método não é necessário nenhum comando ou classe especial, pode ser uma atribuição de valores, uma consulta a um banco de dados ou qualquer coisa. O ponto principal é que para que este método seja executado em uma thread separada e ficando assim em segundo plano, enquanto outros métodos possam ser executados em paralelo basta usar o código a seguir:

var thread = new System.Threading.Thread(MeuMetodo);thread.Start();

Note que este método não retorna nada e nem requer parâmetros, porém, podemos criar métodos que usam parâmetros e tenham algum retorno. A classe System.Threading.Thread é muito versátil e possui métodos para cancelar, suspender e continuar sua execução. Se você puder, verifique a documentação sobre a classe e o seu namespace para conhecer melhor os recursos que podem ser usados, embora, mais à frente, veremos outra forma de controlar execução de threads que permite mais controle com menos complexidade.

Dificuldades ao utilizar Threading

O maior problema ao se usar threads e a classe Thread do Framework .NET é a dificuldade em controlar o seu andamento e também de oferecer ao usuário maneiras realmente eficazes de cancelamento. Isto ocorre porque o desenvolvedor é quem fica responsável por realizar estas tarefas e implementar da maneira que achar melhor. De uma maneira bem direta, é preciso começar tudo do zero porque apesar de toda a flexibilidade oferecida, o programador é quem decide como fazer o controle.

Dois pontos fundamentais para o desenvolvimento assíncrono são reportar o andamento da tarefa e permitir o seu cancelamento. Para que isto ocorra, deve se criar uma forma de comunicação entre o processo que chamou a thread e ela. Uma das formas de fazer a comunicação do andamento e passar para a thread um objeto que possa ser notificado sempre que necessário como um delegate usado em conjunto com um EventHandler.

Delegates funcionam como “ponteiros” para métodos e normalmente são usados juntamente com ponteiros para que uma rotina dentro de uma classe possa executar uma ação quando algo acontecer como, por exemplo, for necessário reportar o progresso do trabalho.

Assim, vamos considerar uma classe, como a Listagem 2 demonstra.

Listagem 2. Demonstrando uso de delegates 1 public class foo 2 { 3 public delegate void FazAlgo(int valor); 4 public event FazAlgo Executa; 5 6 public void Faz() 7 { 8 for (int i = 0; i < 1000; i++) 9 if (i % 100 == 0 && Executa != null) 10 Executa(i); 11 } 12 }

Nesta classe a linha 3 cria o delegate que é a assinatura para o método que será acoplado ao evento. O assessor deve sempre ser public seguido da palavra-chave delegate. O tipo de retorno pode ser qualquer um, porém, o mais comum é void. O delegate pode ou não ter parâmetros. Novamente o usual é que se tenham estes já que seu uso só é justificável caso se deseje passar valores entre rotinas diferentes.

Um delegate precisa estar acompanhado de um evento que será inspecionado e usado para fazer a chamada ao método que foi anexado ao delegate. Neste exemplo, a linha 4 cria um evento chamado Executa. Novamente o assessor deve ser público seguido da palavra event, o tipo é o do delegate criado anteriormente seguido de um nome.

Seguindo em frente a classe tem um método público chamado Faz() que se inicia na linha 6 que teoricamente será executado em segundo plano. Este método executa um loop de zero a mil e a cada cem números passa o valor para um método que vai fazer alguma coisa. Observe na linha 9 um passo importante ao trabalhar com eventos que é a verificação do evento estar nulo. Se o evento não estiver, a classe faz uma chamada ao evento (linha 10).

Para consumir este código, teríamos alguma coisa parecida com o código da Listagem 3.

Listagem 3. Consumindo eventos e delegates 1 using System; 2 3 public class program 4 { 5 public static void Main() 6 { 7 var obj = new foo(); 8 obj.Executa += DisparaFazAlgo; 9 obj.Faz(); 10 } 11 12 public void DisparaFazAlgo(int valor) 13 { 14 Console.WriteLine(valor); 15 } 16 }

Trata-se de um programa que se inicia criando uma instância da classe (linha 7) e na linha 8 acopla o método DisparaFazAlgo() o evento Executa da classe. Em seguida é feita uma chamada ao método Faz() para executar o código. O método DisparaFazAlgo() está descrito a partir da linha 12. Note que o cabeçalho segue o modelo definido pela classe.

Assim, de uma forma bem básica, são estes os passos para que métodos dentro de uma classe possa executar código externo, geralmente fazendo parte da classe um nível acima.

Espero que tenha ficado claro que a principal desvantagem de se fazer tudo do zero é a dificuldade e o número de passos necessários, sem falar da necessidade de estar atendo a muitos detalhes. É importante também notar que, existem situações que somente esta abordagem poderá ser usada em casos de desenvolver rotinas para bibliotecas de funções que serão usadas em múltiplos projetos. Além disto, desenvolver tudo a partir do zero dá a possibilidade de se controlar cada aspecto do código.

Background Worker

Como quase todos os recursos para o Framework .NET existe uma opção mais acessível e que oferece blocos prontos para manipulação de threads. Esta opção está na classe System.ComponentModel.BackgroundWorker que é na verdade uma classe que encapsula o funcionamento de threads com propriedades, eventos e métodos prontos para serem usados em seus projetos de qualquer natureza. Como “projetos de qualquer natureza”, estão incluídas as class libraries (dlls), projetos Windows Forms, WPF e Web. Esta classe está presente nos projetos Windows Forms através de um componente da ToolBox do Visual Studio, mas, pode ser criado diretamente no código.

O seu grande diferencial são recursos mais eficientes (e também mais fáceis de usar) para controle de execução, progresso e cancelamento além de um melhor tratamento do final da thread. Para se ter uma ideia de como o trabalho é realizado são duas as principais propriedades que precisam ser configuradas ao se criar um objeto desta classe:

1. WorkerReportProgress: indica se as rotinas que estão vinculadas com o componente (ou objeto se preferir) vão dar informações sobre o andamento das tarefas vinculadas. Neste caso, ao executar uma chamada para o método ReportProgress do componente, passando o percentual concluído do trabalho e opcionalmente, algum outro dado que for necessário.

2. WorkerSupportsCancellation: dá suporte para que a rotina que está sendo executada seja cancelada de forma assíncrona. Neste caso, ao finalizar o trabalho deve se criar um EventHandler para o evento RunWorkerCompleted que verifique se a rotina foi cancelada.

Como se pode perceber, com poucas propriedades já se tem boa parte do trabalho executado. Como foi citado o acoplamento de eventos, são três os principais eventos que devem ser tratados ao se trabalhar com este componente. O primeiro é o DoWork. Este evento é chamado pelo método RunWorkAsync(). Ao fazer isto é necessário escrever um método com a assinatura para este evento que é a seguinte:

private void worker_DoWork(object sender, DoWorkEventArgs e)

O primeiro parâmetro é usado para capturar de qual objeto partiu a chamada para a rotina assíncrona e o segundo, armazena dois parâmetros que podem ser passados para a rotina:

1. Argument: valor do tipo object podendo armazenar qualquer dado.

2. Result: outra propriedade do tipo object que armazena o resultado da rotina e é enviada para o método executado ao término da tarefa. Use esta propriedade para tratar o retorno de métodos que foram executados em background.

Durante a execução da tarefa, pode ser que seja importante mostrar o andamento do trabalho usando, por exemplo, uma barra de progressos. Um aspecto importante da utilização de rotinas assíncronas ou threads é que estas não podem acessar os componentes da interface diretamente. Isto porque só códigos dentro da thread que criou o controle visual podem interagir com os mesmos. Porém, com o BackgroundWorker isto é diferente, basta escrever um método para o evento ProgressChanged, que deve ter a seguinte assinatura:

private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)"

[...] continue lendo...

Artigos relacionados