Desenvolvimento - C#

WCF - Chamadas Assíncronas

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.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i 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);
}
Imports System
Imports System.ServiceModel

<ServiceContract()> Public Interface ICliente
    <OperationContract()> _
    Function CalcularComissao(codigo As String) As Decimal
End Interface
C# VB.NET

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);
}
Using proxy As New ClienteClient()
    Dim ar As IAsyncResult = proxy.BeginCalcularComissao("123", Nothing, Nothing)

    "fazer algum trabalho

    Dim resultado As Decimal = proxy.EndCalcularComissao(ar)
    Console.WriteLine(resultado)
End Using
C# VB.NET

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.");
    }
}
Using proxy As New ClienteClient()
    Dim ar As IAsyncResult = proxy.BeginCalcularComissao("123", Nothing, Nothing)

    "fazer algum trabalho

    If ar.IsCompleted Then
        Dim resultado As Decimal = proxy.EndCalcularComissao(ar)
        Console.WriteLine(resultado)
    Else
        Console.WriteLine("O processo ainda não finalizou.")
    End If
End Using
C# VB.NET

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);
}
Private _proxy As ClienteClient

Private Sub TestandoProcessoAssincronoComCallback()
    _proxy = New ClienteClient()
    _proxy.BeginCalcularComissao("123", New AsyncCallback(AddressOf Callback), Nothing)
    Console.ReadLine()
End Sub

Private Sub Callback(ByVal ar As IAsyncResult)
    Dim resultado As Decimal = _proxy.EndCalcularComissao(ar)
    Console.WriteLine(resultado)
End Sub
C# VB.NET

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.

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);
}
Imports System
Imports System.ServiceModel

<ServiceContract()> Public Interface ICliente
    <OperationContract(AsyncPattern:=True)> _
    Function BeginRecuperar(ByVal callback As AsyncCallback, ByVal state As Object) _
        As IAsyncResult

    Function EndRecuperar(ByVal ar As IAsyncResult) As Cliente()
End Interface
C# VB.NET

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 forma 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();
    }
}
Imports System
Imports System.Threading
Imports System.Data.SqlClient
Imports System.Collections.Generic

Public Class ServicoDeClientes
    Implements ICliente

    Private Const SQL_CONN_STRING As String = "...;Asynchronous Processing=True"
    Private _connection As SqlConnection
    Private _command As SqlCommand

    Public Function BeginRecuperar(ByVal callback As AsyncCallback, ByVal state As Object) _
        As IAsyncResult Implements ICliente.BeginRecuperar

        Me._connection = New SqlConnection(SQL_CONN_STRING)
        Me._command = New SqlCommand("SELECT Nome FROM Cliente", Me._connection)

        Me._connection.Open()
        Return Me._command.BeginExecuteReader(callback, state)
    End Function

    Public Function EndRecuperar(ByVal ar As IAsyncResult) As Cliente() _
        Implements ICliente.EndRecuperar

        Dim resultado As New List(Of Cliente)()

        Using Me._connection
            Using Me._command
                Using dr As SqlDataReader = Me._command.EndExecuteReader(ar)
                    While dr.Read()
                        resultado.Add(New Cliente() With {.Nome = dr.GetString(0)})
                    End While
                End Using
            End Using
        End Using

        Return resultado.ToArray()
    End Function
End Class
C# VB.NET

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.

Faça o download do exemplo.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.