Muitas vezes desenvolvemos um método para desempenhar alguma tarefa e, depois de devidamente codificado, invocamos o mesmo a partir de algum ponto da aplicação. Dependendo do que este método faz, ele pode levar certo tempo para executar e, se o tempo for consideravelmente alto, podemos começar a ter problemas na aplicação, pois como a chamada é sempre realizada de síncrona, enquanto o método não retornar, a execução do sistema que faz o uso do mesmo irá congelar, aguardando o retorno do método para dar seqüência na execução.
Entretanto, para fornecer uma melhor experiência ao usuário e tornar o desempenho da aplicação muito mais performático, podemos optar por executar, uma determinada tarefa de forma assíncrona. Isso irá possibilitar o disparo de um processo custoso em uma thread de trabalho a parte da aplicação, permitindo assim que a aplicação continue funcionando enquanto o processo custoso é executado em paralelo e, quando o mesmo finalizar, podemos recuperar o resultado e exibir o mesmo ao usuário.
A finalidade deste artigo é mostrar como implementar o processamento assíncrono tanto do lado do cliente (proxy) bem como do lado do servidor (contrato) em serviços WCF. Antes de prosseguir, é necessário que estejamos familiarizados com a arquitetura de processamento assíncrono dentro da plataforma .NET. Basicamente, temos alguns métodos dentro do próprio .NET Framework que existem 3 versões para o mesmo: o primeiro deles é para uso síncrono; já os outros dois (BeginXXX e EndXXX) são para a chamada assíncrona deste mesmo processo. Convencionou-se que métodos síncronos são nomeados de acordo com a sua funcionalidade, exemplo: Read, Write; já, as versões assíncronas destes mesmos métodos são: BeginRead, EndRead, BeginWrite e EndWrite.
A versão 2.0 do ADO.NET também já traz suporte ao processamento assíncrono para tarefas comuns de acesso a dados, como a execução de queries complexas para modificação de registros no banco de dados e também quando necessitamos recuperar dados com alguma query mais complexa (mais detalhes podem ser vistos neste artigo). O suporte ao processo assíncrono não para por aí, acesso aos arquivos no disco, acesso a serviços e até mesmo os delegates trazem nativamente esse suporte.
Processamento Assíncrono no Cliente
Neste cenário, nada é necessário ser realizado durante a criação e implementação do contrato. Isso funcionará de forma muito semelhante ao que já acontecia com os artigos ASP.NET Web Services, ou seja, para cada método criado no contrato, é também criado uma versão assíncrona (BeginXXX e EndXXX) do método quando fazemos a referência ao serviço no cliente. Aqui há ligeira mudança: os métodos que dão suporte assíncrono não são implicitamente criados. A imagem abaixo ilustra o local que devemos marcar para definir a criação dos métodos que dão suporte ao processo assíncrono:
|
Figura 1 – Ao efetuar uma Add Service Reference, clique na opção Advanced para marcar a opção que está marcada em vermelho. |
Por padrão, esta opção não está marcada. Uma vez que você opta por marcá-la, será criada uma versão assíncrona para cada método que há dentro do contrato. A partir daí o desenvolvedor que consume o proxy do serviço precisará se atentar para efetuar a chamada para o método de forma assíncrona, o que exigirá a forma em que a chamada será executada. Quando se trata um processo assíncrono, temos alguns cuidados a respeito do disparo do método: o método que dá início ao processo (BeginXXX) geralmente espera os parâmetros necessários para a execução da tarefa (caso exista), uma instância de um delegate do tipo AsyncCallback (explorado mais abaixo) e, finalmente, um object que representa uma informação “contextual”.
Observação: A opção “Generate asynchronous operations” é o mesmo que especificar o parâmetro /async do utilitário svcutil.exe.
Para exemplificar, temos um contrato chamado ICliente que possui um método chamado CalcularComissao que, por sua vez, retornará um valor que representa o valor calculado das comissões. Essa tarefa trata-se de um processo que, para fins de exemplo, será encarado como uma tarefa custosa e que levará certo tempo para executar. O código abaixo ilustra o contrato a ser utilizado (note que não há nada de especial):
using System; using System.ServiceModel; [ServiceContract] public interface ICliente { [OperationContract] decimal CalcularComissao(string codigo); } |
Uma vez adicionado à referência, podemos notar que haverá três métodos a nossa disposição do lado do cliente: CalcularComissao para processo síncrono, BeginCalcularComissao e EndCalcularComissao para processamento assíncrono. Durante a geração do proxy o método BeginXXX é decorado com o atributo OperationContractAttribute que, por sua vez, tem uma propriedade chamada AsyncPattern e, neste caso, está definida como True. Com essa configuração, o runtime não invocará o método BeginCalcularComissao do contrato, mesmo porque ele não existe; na verdade o runtime irá utilizar uma thread do ThreadPool para disparar sincronamente o método definido pela propriedade Action e, que neste caso é o método CalcularComissao.
Vou supor que a chamada para o método de forma síncrona já é conhecida e está sedimentada. O foco aqui será entender o funcionamento do processo assíncrono. O ponto de partida é o método BeginCalcularComissao: ele dará início ao processo e, além de receber o callback e o object como parâmetro (como já foi dito acima), esse método também espera os parâmetros que o próprio método exige para desempenhar a tarefa a qual ele se destina a fazer. Seguindo a arquitetura de processamento assíncrono do .NET Framework, o método BeginCalcularComissoes retorna um objeto que implementa a Interface IAsyncResult; esse objeto armazena uma referência para o processo que está sendo executado em paralelo.
Como já pudemos notar, não é o método BeginXXX que retorna o resultado. Isso é uma tarefa que pertence exclusivamente ao método EndXXX. A questão é quando invocá-lo. Uma vez que você invoca o mesmo, e o processo ainda não tenha sido finalizado, o programa trava a execução, esperando pelo retorno do processo. Dependendo do quanto tempo ainda falta para finalizar, podemos cair no mesmo problema do processamento síncrono. O trecho de código abaixo ilustra como consumir o método BeginCalcularComissoes:
using (ClienteClient proxy = new ClienteClient()) { IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null); //fazer algum trabalho decimal resultado = proxy.EndCalcularComissao(ar); Console.WriteLine(resultado); } |
No exemplo acima podemos ter algum benefício, pois o método BeginCalcularComissao dispara o processamento assíncrono e já devolve o controle da execução para o programa e, conseqüentemente, permite que façamos algum trabalho em paralelo enquanto o serviço calcula as comissões. O problema é que se no momento da chamada do método EndCalcularComissao (que retornará o resultado) o processo ainda não finalizou, a execução irá bloquear a execução até que o processo assíncrono retorne. Vale lembrar que essa técnica às vezes é necessária: imagine um momento em que o programa não pode continuar a sua execução, pois depende o resultado do processo assíncrono para continuar.
Há ainda uma outra possibilidade de testar o retorno do processo assíncrono, que é chamado de pooling. Essa técnica consiste em antes de invocar o método EndCalcularComissao e possivelmente bloquear a execução, podemos testar se o processo finalizou ou não. Se repararmos no exemplo de código acima, o método BeginCalcularComissao retorna um objeto que implementa a Interface IAsyncResult. Essa Interface fornece um método chamado IsCompleted que retorna um valor booleano indicando se o processo foi ou não finalizado. Isso garantirá que chamaremos o método EndCalcularComissao somente que o processo realmente finalizou. O código abaixo ilustra o uso desta propriedade:
using (ClienteClient proxy = new ClienteClient()) { IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null); //fazer algum trabalho if (ar.IsCompleted) { decimal resultado = proxy.EndCalcularComissao(ar); Console.WriteLine(resultado); } else { Console.WriteLine("O processo ainda não finalizou."); } } |
Finalmente, a última técnica que temos para disparar um método de forma assíncrona é com a utilização de callbacks. Com esta alternativa, ao invés de ficarmos testando se o processo finalizou ou não, ao invocar o método BeginCalcularComissao, passamos uma instância da classe AsyncCallback com a referência para um método do lado do cliente que deve ser disparado com o processo assíncrono for finalizado.
Uma vez especificado o método que será utilizado como callback, não é mais necessário que você armazene o objeto que armazena a referência para o processo assíncrono (IAsyncResult), pois isso será automaticamente fornecido para o callback; além disso, ainda é importante dizer que o método de callback é executado sob a thread de trabalho, antes dela ser devolvida para o ThreadPool. Como já foi comentado acima, informamos o método a ser disparado a partir de uma instância do delegate AsyncCallback, como é mostrado no código abaixo:
private static ClienteClient _proxy; private static void TestandoProcessoAssincronoComCallback() { _proxy = new ClienteClient(); _proxy.BeginCalcularComissao("123", new AsyncCallback(Callback), null); Console.ReadLine(); } private static void Callback(IAsyncResult ar) { decimal resultado = _proxy.EndCalcularComissao(ar); Console.WriteLine(resultado); } |
Observação: Podemos recorrer à utilização de métodos anônimos ou até mesmo das expressões lambda para evitar a criação de um método a parte para ser disparado quando o callback acontecer.
Além do tradicional modelo de chamadas assíncronas (APM), temos a possibilidade da chamada assíncrona baseada em eventos. A idéia aqui é, antes de invocar a operação, você poderá assinar à um evento que será disparado somente quando o processo assíncrono finalizar. Isso evitará de ter um trabalho manual para analisar se o processo finalizou ou não (poll, waiting, etc.). Internamente durante a geração do proxy, o código que é auto-gerado já inclui a implementação necessária para o modelo baseado em eventos.
Basicamente é criado mais uma versão do método, agora com o sufixo XXXAsync que, internamente, faz a chamada para os métodos BeginXXX/EndXXX que, como já sabemos, dispararam a operação de forma assíncrona. Além disso, um delegate do tipo EventHandler<T> também será criado para representar o callback que, quando disparado, invocará o evento do lado de quem está consumindo o serviço. Abaixo um exemplo de como efetuar a chamada assíncrona baseada em eventos:
using (ClienteClient proxy = new ClienteClient()) { proxy.CalcularComissaoCompleted += new EventHandler<CalcularComissaoCompletedEventArgs>(proxy_CalcularComissaoCompleted); proxy.CalcularComissaoAsync("2"); Console.ReadLine(); } private static void proxy_CalcularComissaoCompleted(object sender, CalcularComissaoCompletedEventArgs e) { Console.WriteLine("Fim."); } |
Se notarmos a implementação interna do proxy, veremos que o método XXXAsync faz o uso do método InvokeAsync, da classe ClientBase<T>. Este método está disponível somente a partir do .NET Framework 3.5. Sendo assim, alguns detalhes durante a geração do proxy precisam ser analisados:
-
Via “Add Service Reference”: se voce estiver fazendo a referencia em um projeto que esteja utilizando o .NET Framework 3.5 e voce opta pela geração dos métodos que dão suporte ao processamento assíncrono, ele também criará os tipos necessários para suportar o modelo de eventos.
-
Via svcutil.exe: neste caso voce precisará especificar através do parametro /async e, além disso, especificar a versão do .NET Framework através do parâmetro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.
Processamento Assíncrono no Servidor
Tudo que vimos acima consiste em permitir que o cliente que consome o serviço execute o método de forma assíncrona, evitando que a aplicação não seja bloqueada enquanto o método está sendo executado. A partir de agora iremos analisar como implementar o processamento assíncrono no servidor. Isso consistirá em uma mudança considerável no contrato do serviço e que veremos mais abaixo.
A finalidade do processo assíncrono do lado do servidor é permitir aumentar a escalabilidade e a performance sem afetar os clientes que consomem o serviço. É importante dizer que os processos assíncronos do lado do cliente e do lado do servidor trabalham de forma independente, visando benefícios totalmente diferentes. A idéia do processamento assíncrono do lado do servidor visa principalmente a execução de tarefas que tem um custo alto para execução e, alguns casos comuns, são consultas a alguma informação no banco de dados, leitura/escrita de arquivos no disco, etc.
Isso permitirá a liberação da thread que está executando o serviço seja liberada enquanto o processo custoso acontece em paralelo, permitindo à você executar alguma tarefa em paralelo enquanto o processo ocorre ou até mesmo não ter threads sendo bloqueadas enquanto aguarda este processamento. Mais uma vez, você pode combinar este recurso as técnicas de processamento assíncrono que já existem dentro do .NET Framework, como é o caso do ADO.NET 2.0, classes do namespace IO, etc. Para o exemplo, iremos simular um processo custoso na base de dados.
Como já foi dita acima, o contrato do serviço mudará. Precisaremos desenhá-lo para se enquadrar no padrão de processamento assíncrono do .NET Framework que, como já sabemos, para cada método, teremos na verdade um par onde, o primeiro corresponde ao início da execução (BeginXXX) e, o segundo, corresponde à finalização do processamento (EndXXX). O código abaixo ilustra como devemos proceder para definirmos tais métodos:
using System; using System.ServiceModel; [ServiceContract] public interface ICliente { [OperationContract(AsyncPattern = true)] IAsyncResult BeginRecuperar(AsyncCallback callback, object state); Cliente[] EndRecuperar(IAsyncResult ar); } |
O código acima ilustra exatamente os passos que devemos seguir para possibilitar a chamada assíncrona pelo WCF. Os métodos que desejamos que sejam invocados assincronamente pelo WCF devem, ao invés de ter apenas um único método para realizar a tarefa, devemos obrigatoriamente dividi-los em duas partes (métodos): BeginXXX e EndXXX. É importante dizer que apenas o método BeginXXX será decorado com o atributo OperationContractAttribute, definindo a propriedade AsyncPattern para True. Ainda seguindo o padrão do processamento assíncrono do .NET Framework, o método Begin deve receber como parâmetro um objeto do tipo AsyncCallback (que mais tarde apontará para o método EndXXX) e um Object, retornando um objeto que implementa a Interface IAsyncResult. Já o método EndXXX deverá efetivamente retornar a informação que o método destina-se a fazer e, como parâmetro, deve receber um objeto que implementa a Interface IAsyncResult.
Quando o contrato acima for exposto pelo WCF e algum cliente consumi-lo, apenas terá um único método chamado Recuperar. Como foi dito anteriormente, a idéia aqui é evitar que o cliente saiba como isso está implementado dentro do serviço. Neste caso, não faz sentido você ter “uma versão síncrona do método” e, caso tenha, por padrão, o WCF sempre irá utilizá-la.
Uma vez que o contrato esteja definido, devemos implementá-lo na classe e, conseqüentemente, expor a mesma para ser consumida. A implementação da Interface ICliente assim como qualquer outra que exige o processamento assíncrono, demanda alguns cuidados no momento da implementação em relação ao formato tradicional. Como estamos utilizando o ADO.NET 2.0 e o mesmo já traz nativamente o suporte assíncrono a execução de queries de forma assíncrona, podemos integrá-lo a execução:
using System; using System.Threading; using System.Data.SqlClient; using System.Collections.Generic; public class ServicoDeClientes : ICliente { private const string SQL_CONN_STRING = "...;Asynchronous Processing=True"; private SqlConnection _connection; private SqlCommand _command; public IAsyncResult BeginRecuperar(AsyncCallback callback, object state) { this._connection = new SqlConnection(SQL_CONN_STRING); this._command = new SqlCommand("SELECT Nome FROM Cliente", this._connection); this._connection.Open(); return this._command.BeginExecuteReader(callback, state); } public Cliente[] EndRecuperar(IAsyncResult ar) { List<Cliente> resultado = new List<Cliente>(); using (this._connection) using(this._command) using (SqlDataReader dr = this._command.EndExecuteReader(ar)) while (dr.Read()) resultado.Add(new Cliente() { Nome = dr.GetString(0) }); return resultado.ToArray(); } } |
No método que inicia o processo abrimos a conexão com a base de dados, informamos a query e, finalmente, invocamos o método BeginExecuteReader passando o callback e o object que é passado como parâmetro para o método BeginRecuperar. Finalmente, o método EndRecuperar é invocado e através dele coletamos o resultado da execução do processo pesado, preparamos o mesmo e retornamos. Este será o resultado que será encaminhado ao cliente.
O código do lado do cliente não muda em nada. Independentemente da implementação que você utilize do lado do servidor, do lado do cliente nada mudará, ou seja, mesmo que você crie dois métodos (Begin e End) para compor o processamento assíncrono, o WCF sempre disponibilizará um único método para o cliente. Ele poderá invocá-lo de forma síncrona ou assincronamente.
Conclusão: No decorrer deste artigo pudemos entender como integrar o processamento assíncrono em serviços WCF. É possível realizar essa técnica em ambos os lados (cliente e servidor), mas é importante lembrar que os mesmos trabalham de forma independente que, apesar de serem semelhantes, tem finalidade completamente diferente e, sendo assim, necessitamos entender o contexto e aplicar a implementação ideal.
Muito bom o post. Vi o vídeo e agora o artigo sobre o tema. Completíssimo.