Se quiser visualizar toda a Stack Trace de uma aplicação .NET que está sendo executada (incluindo processos como o w3wp.exe), podemos recorrer à um utilitário gratuito chamado Managed Stack Explorer.
Arquivo mensal: setembro 2008
O tipo de dado também é um validador
Analisando o projeto de um terceiro me deparei com um código que, ao meu ver, está errado (o exemplo não segue o padrão de nomenclatura encontrado lá):
public void DefinirDataDePagamento(DateTime? data)
{
if(data == null)
throw new ArgumentNullException(“data”);
//executa a tarefa
}
Pelo que aparenta, a data que é passada como parametro para o método DefinirDataDePagamento é obrigatória e, caso seja nula, uma exceção do tipo ArgumentNullException é disparada. Se essa regra existe (não permitir que data seja nula), então porque o parametro data é criado como sendo um Nullable<DateTime> ao invés de apenas DateTime? Se fosse definido como DateTime, obrigatoriamente, o desenvolvedor deverá passar uma data (seja DateTime.MinValue, DateTime.MaxValue, DateTime.Now ou um DateTime customizado) ao utilizar o método.
WCF, IIS e Threads
Sabemos que podemos utilizar o IIS como hosting para um serviço WCF. O Visual Studio 2008 já traz uma template chamada WCF Service. A finalidade desta template é possibilitar a criação de vários serviços, onde cada um será representado por um arquivo com a extensão *.svc. Depois de desenvolvido, podemos publicar este serviço em um servidor com IIS e com o .NET Framework devidamente instalado.
O WCF fornece dois tipos não documentados dentro do namespace System.ServiceModel.Activation, a saber: HttpHandler e HttpModule. A finalidade destes tipos é acoplá-los ao pipeline do ASP.NET e, quando uma requisição chegar para um serviço WCF (*.svc), o ASP.NET irá desviar a execução para o HttpHandler.
Um grande problema que ocorre neste procedimento é com relação ao uso de threads. Como a requisição é inicialmente passada pelo ASP.NET e, quando ele detecta que se trata de um serviço WCF, uma thread de I/O é criada para executar o serviço mas, a worker thread que é utilizada por tratar as requisições do ASP.NET, continuará bloqueada até que a requisição à operação esteja completa. Isso impedirá que a worker thread atenda à outras requisições enquanto espera que o processo seja finalizado.
A configuração padrão do WCF (mesmo depois do SP1 instalado) continua utilizando esta mesma técnica, ou seja, o processamento síncrono da operação. Mas, a partir do SP1 do .NET Framework 3.5, foram introduzidos mais dois tipos que tem o papel de executar, de forma assíncrona, as operações que chegam pelo ASP.NET ao WCF. Esses novos tipos são: ServiceHttpModule e ServiceHttpHandlerFactory.
Com estes dois novos tipos acoplados no pipeline do ASP.NET, permitirá que a execução seja feita de forma assíncrona, liberando a worker thread utilizada pelo ASP.NET para atender a outras requisições. Para habilitar o processamento assíncrono, podemos utilizar uma ferramenta chamada WcfAsyncWebUtil.exe, desenvolvida pelo Wenlong Dong.
É importante dizer que há uma relação entre o processamento assíncrono do runtime do ASP.NET/WCF com o contrato assíncrono (AsyncPattern = true). Se combinarmos o handler/módulo assíncrono com o contrato assíncrono, nenhuma thread será consumida durante a espera pelo processamento assíncrono da operação. Mesmo quando o contrato não é assíncrono, ao utilizar o handler/módulo assíncrono, teríamos uma diminuição de threads rodando concorrentemente e assim, tendo um ganho considerável.
TechEd Brasil 2008
Bem, como todo mundo já sabe, de 14 à 16 de outubro acontece em São Paulo o TechEd Brasil e, como no ano passado, este ano também estarei efetuando uma palestra na sala 7 no dia 15/10, das 11:00 às 12:15hs. O tema da palestra é:
WEB401 – Programação Assíncrona do ASP.NET 2.0
Nível: 400
Um dos principais pontos para a implementação de aplicações ASP.NET escaláveis, é o uso de pool de threads do ASP.NET da maneira mais eficiente possível. Isto significa evitar situações onde threads fiquem: aguardando o retorno de consultas à base de dados, chamadas à Web Services e que operações de I/O terminem. Nesta sessão descreveremos como ajustar os três modelos de programação assíncrona em ASP.NET. Se você está preocupado em construir sites Web com alta escalabilidade assista essa sessão.
Pré-Requisitos: Como pré-requisito para esta palestra é importante que o ouvinte tenha conhecimento nas classes de acesso à banco dados, uso de proxy para chamadas à Web Services e, se possível, familiaridade com o modelo de programação assíncrona (APM) do .NET Framework (Begin/End).
Além da palestra, estarei também participando do Ask The Experts, que é um bate-papo informal sobre determinadas tecnologias. Estarei na seção onde será abordado sobre ASP.NET, AJAX, Silverlight e Windows Live. Para aqueles que estiverem no evento e, se estiver disposto a assistir a palestra ou discutir sobre alguma tecnologia, fiquem a vontade para me procurar e, conseqüentemente, conversar à respeito dos assuntos relacionados a palestra e/ou algo sobre .NET.
WCF – Throttling e Pooling
Através do gerenciamento de instância de um serviço podemos definir qual a forma de criação de uma instância para servir uma determinada requisição. Essa configuração que fazemos à nivel de serviço, através de um behavior, não impõe nenhuma restrição na quantidade de instância e/ou execuções concorrentes que são realizadas e, dependendo do volume de requisições que o serviço tenha ou até mesmo a quantidade de recursos que ele utiliza, podemos degradar consideravelmente a performance.
O Throttling possibilita restringirmos a quantidade de sessões, instâncias e chamadas concorrentes que são realizadas para um serviço. Além do Throttling, ainda há outra funcionalidade que pode ser utilizada em um serviço, que é o Pooling de objetos, muito comum dentro do Enterprise Services (COM+). Este artigo explicará como proceder para efetuar a configuração do Throttling e suas implicações; também falaremos supercialmente sobre a estrutura do Pooling e como implementá-lo.
Throttling
Seria muito interessante conseguir atender a todas as requisições que chegam para um serviço mas infelizmente, devido à limitação de alguns recursos, isso nem sempre será possível. Cada serviço (assim como qualquer outra aplicação) está limitado à disponibilidade do processador, memória, conexão com base de dados, etc. Um grande número de chamadas faz com que um grande número de instâncias sejam criadas ou um grande número de acesso concorrente (dependerá do modo de gerenciamento de instância escolhido). Cada instância ou cada thread exige recursos do sistema para poder realizar a operação.
Para sanar problemas como estes temos duas alternativas: a primeira deleas é ter um hardware mais potente e que consiga atender as todas as requisições mais, se a quantidade de requisições aumentar, precisará de mais hardware; já a segunda alternativa é restringir o acesso à tais recursos, limitando o número de chamadas concorrentes e/ou sessões ativas. Caso o limite for atingido, as requisições serão enfileiradas, aguardando a sua vez ou, se essa espera pelo processamento demorar muito tempo, falhará por timeout.
O WCF te possibilita controlar esses limites através do Throttling. Basicamente a idéia do Throttling é limitar o número de sessões, instâncias e execuções concorrentes de um serviço. Essa configuração é realizada a partir de um behavior de serviço, ou seja, essa configuração refletirá na(s) instância(s) do serviço, independente de endpoints. Temos três propriedades que podemos definir para especificar esse comportamento:
-
MaxConcurrentInstances: Esta propriedade especifica o número máximo de instâncias concorrentes permitidas para o serviço. Quando o modo de gerenciamento de instância está definido como Single, esta informação é irrelevante, pois existirá apenas uma única instância servindo todas as requisições; já no modo PerCall o número de instâncias será o mesmo número de chamadas concorrentes.
-
MaxConcurrentCalls: Esta propriedade especifica o número máximo de mensagens concorrentes em que o serviço poderá processar. O valor padrão é definido como 16.
-
MaxConcurrentSessions: Esta propriedade especifica o número máximo de sessões concorrentes que o serviço permitirá. É importante lembrar que os clientes são responsáveis por inicializar ou terminar as sessões, realizando várias chamadas entre esse período. O problema das sessões é que quando elas são longas, ou melhor, duram muito tempo, outros clientes podem ser bloqueados. Os bindings que estão sob HTTP e estão com a sessão desabilitada não interferem no valor, pois não mantém uma conexão ativa com o serviço e, conseqüentemente, não haverá instância servindo-o. O valor padrão é definido como 10.
A configuração do Throttling afeta diretamente o serviço, independente de que endpoint a requisição está vindo. Por isso, a configuração poderá ser realizada via arquivo de configuração ou de forma imperativa. No modelo declarativo utilizamos o elemento serviceThrottling, que faz parte de um behavior que está diretamente ligado à um serviço; já no modelo imperativo, utilizamos a classe ServiceThrottlingBehavior (namespace System.ServiceModel.Description) que, depois de configurada, a adicionamos à coleção de behaviors, exposta pela classe ServiceHost. O trecho de código abaixo exemplifica as duas formas de configurar o Throttling:
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using(ServiceHost host = new ServiceHost(typeof(ICliente),
new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
//Configuração dos Endpoints
ServiceThrottlingBehavior t = new ServiceThrottlingBehavior();
t.MaxConcurrentCalls = 40;
t.MaxConcurrentInstances = 20;
t.MaxConcurrentSessions = 20;
host.Description.Behaviors.Add(t);
host.Open();
Console.ReadLine();
}
|
A configuração do Throttling deve ser realizada antes da abertura do serviço. A configuração equivalente ao que vimos acima, mas utilizando o arquivo de configuração é mostrado abaixo:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="Host.ServicoDeClientes" behaviorConfiguration="Config">
<!-- Endpoints -->
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="Config">
<serviceThrottling
maxConcurrentCalls="40"
maxConcurrentInstances="20"
maxConcurrentSessions="20" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
|
Pooling
Já sabemos que o runtime do WCF cria instâncias da classe que representa o serviço para atender uma determinada requisição. A criação das instâncias dependerá do modo de gerenciamento escolhido, que foi constantemente abordado neste artigo. Para relembrar, o modo PerSession cria uma instância para cada cliente; já o modo PerCall cria uma instância por chamada e, finalmente, o modo Single cria uma única instância.
Criar e destruir instâncias a todo momento pode ser extremamente custoso, pois às vezes existem tarefas árduas que são realizadas no momento da sua inicialização. Para ter um ganho considerável em performance, podemos recorrer à uma técnica chamada de Pooling. Esta técnica consiste em, ao invés de destruir o objeto por completo e removê-lo da memória colocá-lo em um repositório para reutilizá-lo. Essa técnica também terá influencia do modo de gerenciamento de instâncias.
Quando uma requisição chega para um serviço que está exposto via modo PerCall, o WCF verifica se há objetos “pré-criados” no pool para atender a requisição. Caso exista, ele utilizará essa instância para servir a requisição. Quando a requisição encerrar, o objeto será devolvido para o pool, podendo ser reaproveitado para uma futura requisição. Quando utilizarmos o modo PerSession, a semântica é a mesma, mas o objeto somente será retornado ao pool quando o cliente encerrar a sessão. Já o modo Single não se aplica ao pooling. Por questões de segurança e também de consistência, ao retornar um objeto para o pool, os dados que são utilizados pelo mesmo (campos internos) devem ser reinicializados.
Por padrão, o WCF não suporta nativamente a técnica de Pooling. Com toda a estensibilidade (behavior) que o WCF possui, fica fácil acoplar uma extensão ao mesmo para suportar o Pooling. Assim como o cliente possui o proxy, o serviço possui o dispatcher*. Para cada endpoint temos um endpoint dispatcher relacionado, sendo ele o responsável por converter as mensagens que chegam para o serviço (mais especificamente para o endpoint) em chamadas para as operações que o serviço fornece e, depois disso, retornar uma mensagem contendo a resposta.
* O cliente também pode criar um dispatcher, mas isso acontece quando estamos utilizando um contrato Duplex e está fora do escopo deste artigo.
O endpoint expõe uma propriedade chamada DispatchRuntime, do tipo DispatchRuntime, que representa o dispatcher. Esta classe, por sua vez, fornece uma propriedade chamada InstanceProvider, que é onde podemos acoplar a instância do objeto que fará a extração e a devolução das classes (instâncias) do serviço ao pool. Essa propriedade recebe um objeto que implementa a Interface IInstanceProvider (namespace System.ServiceModel.Dispatcher) e a utilizaremos quando formos definir nosso próprio mecanismo de criação de instâncias.
A Interface IInstanceProvider disponibiliza três membros, os quais devem obrigatoriamente ser implementados na classe que gerenciará o pool. Abaixo temos uma tabela com a explicação para esses três membros, mas é importante dizer que a freqüência para a chamada dos métodos abaixo está condicionada ao modo de gerenciamento de instâncias utilizado pelo serviço (propriedade InstanceContextMode do atributo ServiceBehaviorAttribute):
|
A implementação do pool consiste em duas etapas: a primeira delas é a criação da classe que gerenciará o pool e uma classe para acoplarmos ao runtime do WCF. Como já sabemos, a classe que irá gerenciar o pool deve implementar a Interface IInstanceProvider; para facilitar o trabalho, iremos criá-la de forma genérica. Internamente ela manterá um membro estático chamado _pool do tipo Stack (coleção do tipo LIFO (Last-In First-Out)), que armazenará as instâncias para serem reutilizadas. Como já é de se esperar, o método GetInstance irá verificar se existe ou não algum item disponível dentro desta coleção e, caso exista, ele será utilizado; caso contrário, uma nova instância será criada (você não precisa adicioná-la à coleção antes de utilizá-la, pois quem fará isso será o método ReleaseInstance).
A implementação do pool para efeito de exemplo não é muito complexa. Utilizamos os métodos Pop e Push da Stack para remover ou adicionar um item, utilizando cada um deles no momento propício (GetInstance e ReleaseInstance). Além disso, a classe está tipificada com TService, obrigando que a mesma seja uma classe e também possua um construtor padrão. Além dos métodos fornecidos pela Interface IInstanceProvider, foi criado um método chamado CreateNewInstance, que retornará uma nova instância da classe que representa o serviço, caso seja necessário. O código abaixo exibe a classe na íntegra:
using System;
using System.Diagnostics;
using System.Collections;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
public class PoolInstanceProvider<TService> :
IInstanceProvider where TService : class, ICleanup, new()
{
private static object _lock = new object();
private static Stack _pool;
static PoolInstanceProvider()
{
_pool = new Stack();
}
public object GetInstance(InstanceContext instanceContext, Message message)
{
object obj = null;
lock (_lock)
{
if (_pool.Count > 0)
{
obj = _pool.Pop();
Debug.WriteLine("From Pool");
}
else
{
obj = CreateNewInstance();
Debug.WriteLine("New Object");
}
}
return obj;
}
public object GetInstance(InstanceContext instanceContext)
{
throw new NotImplementedException();
}
public void ReleaseInstance(InstanceContext instanceContext, object instance)
{
lock (_lock)
{
((ICleanup)instance).Cleanup();
_pool.Push(instance);
}
}
private static object CreateNewInstance()
{
return new TService();
}
}
|
Como podemos notar, a classe PoolInstanceProvider faz o uso de um tipo genérico chamado TService, onde TService deve ser tipificado com uma classe que representará o serviço. Há algumas condições que o tipo que é especificado no parâmetro TService deverá satisfazer: class (deve ser uma classe), ICleanup (deve implementar esta Interface) e new (deve ter um construtor sem parâmetros). A Interface ICleanup fornece um método chamado Cleanup, que tem a finalidade de restaurar os dados (membros internos) utilizados por uma instância antes de retorná-la para o pool.
É importante lembrar que a classe acima é apenas um exemplo que, para um mundo real, talvez precisaria ser melhorada. Com ela criada, nos resta definir o ponto de entrada para acoplarmos esta classe dentro do WCF. Assim como o Throttling, o Pooling também deverá refletir para o serviço como um todo, independente de qual endpoint a requisição venha e, devido à isso, criaremos um behavior de serviço, o que nos leva a implementar a Interface IServiceBehavior em uma classe, mais especificamente o método ApplyDispatchBehavior. Esse método fornece a possibilidade de inserir objetos de estensibilidade na execução do WCF, permitindo modificar ou inspecionar o host que está sendo construído para a execução do objeto que representará o serviço.
A instância do host é fornecida como parâmetro para este método e, através dela, temos a coleção de dispatchers relacionados ao mesmo (lembrando que existe um dispatcher para cada endpoint). Inicialmente iremos percorrer a coleção de dispatchers fornecida pelo host e, a partir daí, a coleção de endpoints. Cada endpoint fornece uma propriedade chamada DispatchRuntime que, como vimos acima, é onde o WCF transforma as mensagens em objetos. A partir dessa propriedade podemos vincular a instância do pool que criamos anteriormente, através da propriedade InstanceProvider. É importante dizer que, como o mesmo pool deverá atender à todas as requisições, você deverá definir a mesma instância para todos os endpoints expostos pelo host. O código abaixo exibe como efetuar essa configuração:
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
using System.Collections.ObjectModel;
internal class PoolServiceBehavior<TService> :
IServiceBehavior where TService : class, ICleanup, new()
{
public void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters) { }
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
PoolInstanceProvider<TService> pool = new PoolInstanceProvider<TService>();
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher ed in ((ChannelDispatcher)cdb).Endpoints)
{
ed.DispatchRuntime.InstanceProvider = pool;
}
}
}
public void Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase) { }
}
|
Finalmente, com toda essa infraestrutura pronta, devemos adicionar a instância da classe PoolServiceBehavior à coleção de behaviors do host do serviço. Por se tratar de uma classe genérica, ao criar a sua instância você precisará especificar o mesmo tipo da classe que está sendo exposta pelo serviço. O código abaixo ilustra como devemos proceder para instalar a mesma no host:
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using (ServiceHost host = new ServiceHost(typeof(ServicoDeClientes),
new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
host.Description.Behaviors.Add(new PoolServiceBehavior<ServicoDeClientes>());
//endpoints
host.Open();
Console.ReadLine();
}
|
Observação: Como já sabemos, há outras formas de adicionar um behavior, como por exemplo via arquivo de configuração, ou até mesmo via atributo, mas isso está além do foco deste artigo.
Conclusão: Este artigo explicou como devemos proceder e quais as formas que temos para dizer ao runtime do WCF quando e como criar a instância do objeto que representará o serviço, bem como o impacto que estas técnicas causarão. As configurações que vimos neste artigo (Throttling e Pooling) melhoram consideravelmente a performance de um serviço mas, se utilizado de forma indevida, pode prejudicar ao invés de melhorar.
Chamadas assíncronas à Serviço via modelo de eventos
O WCF fornece, não por padrão, a possibilidade de invocar as operações de um serviço de forma assíncrona. Quando efetuamos a referencia para um serviço via IDE do Visual Studio 2008 ou através do utilitário svcutil.exe (com a opção /async) automaticamente, além da versão síncrona do método, dois métodos chamados BeginXXX/EndXXX (onde XXX é o nome do método síncrono) são adicionados, permitindo que voce invoque assincronamente uma determinada operação.
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, voce 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 p = new ClienteClient())
{
p.CalcularComissaoCompleted +=
new EventHandler<CalcularComissaoCompletedEventArgs>(p_CalcularComissaoCompleted);
p.CalcularComissaoAsync(“2”);
Console.ReadLine();
}
static void p_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 parametro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.
#error
Em alguns momentos durante a escrita de um projeto para servir de exemplo para um artigo, eu gostaria de colocar uma diretiva no código para forçar o leitor a antes de executar o mesmo, configurar algo que seja necessário para que o projeto funcione como o esperado. Depois de algumas pesquisa no MSDN Library, a solução que encontrei foi a diretiva #error, que podemos aplicar ao código da seguinte forma:
#error Altere o valor da connectionstring apontando para uma base válida
private const string SQL_CONN_STRING = “Data Source=.;Initial Catalog=DBTest;Integrated Security=SSPI;”;
Isso impedirá o usuário de compilar a aplicação sem antes alterar essa informação ou, ao menos, comentá-la. 😛
Func vs. Expression
Há algum tempo eu comentei sobre a evolução dos delegates, passando pelas versões 1.0, 2.0 e 3.0 do C#. Sabemos que a partir da versão 3.0 temos uma nova forma de expressar os delegates: expressões lambda. Neste modelo, não há mais necessidade de criar um método adicional ou um método anonimo para executar uma determinada tarefa.
Com a vinda do LINQ, novos delegates também foram introduzidos dentro do namespace System, através do Assembly System.Core.dll:
public delegate TR Func<TR>();
public delegate TR Func<T0, TR>(T0 a0);
public delegate TR Func<T0, T1, TR>(T0 a0, T1 a1);
public delegate TR Func<T0, T1, T2, TR>(T0 a0, T1 a1, T2 a2);
public delegate TR Func<T0, T1, T2, T3, TR>(T0 a0, T1 a1, T2 a2, T3 a3);
Esta família de delegates genéricos servem para construir delegates “on-the-fly”, eliminando a necessidade de criá-los explicitamente. TR representa o resultado do delegate (nunca podendo ser void); depois temos outras versões do mesmo podendo, no máximo, termos quatro parametros de entrada. Em uma operação de soma, poderíamos utilizar o terceiro delegate, como por exemplo:
Func<int, int, int> exemplo = (v1, v2) => v1 + v2;
int resultado = exemplo(2, 3);
Ao compilar este código, o compilador do C# criará: um método que retorna um número inteiro e, no corpo do mesmo, terá o cálculo a ser realizado (v1 + v2) e um delegate que apontará para esse método recém criado; além disso, ele converterá a expressão lambda em um método anonimo, fazendo o uso do delegate criado anteriormente que, neste momento, apontará para o método que fará a soma dos números. O código acima é compilado para:
private static void Main(string[] args)
{
Func<int, int, int> exemplo1 = delegate (int v1, int v2) {
return v1 + v2;
};
int resultado = exemplo1(2, 3);
}
[CompilerGenerated]
private static Func<int, int, int> CS$<>9__CachedAnonymousMethodDelegate1;
[CompilerGenerated]
private static int <Main>b__0(int v1, int v2)
{
return (v1 + v2);
}
Uma outra alternativa é a utilização da classe Expression<TDelegate>, contida dentro do namespace System.Linq.Expressions. Essa classe deve ser tipificada com o mesmo delegate que utilizamos acima e, ao invés de converter a expressão lambda em um código IL que avalia a expressão, irá transformá-la em uma árvore de objetos IL, representando a expressão. Se utilizarmos o mesmo exemplo, veremos que o código mudará:
Expression<Func<int, int, int>> exemplo1 = (v1, v2) => v1 + v2;
int resultado = exemplo1.Compile()(2, 3);
Neste caso, não podemos invocar diretamente o delegate por ele não é um delegate. Essa classe fornece um método chamado Compile que, ao invocá-lo, retorna o delegate especificado na sua criação (Func<int, int, int>) e, a partir daí, podemos utilizá-lo da forma tradicional. Como o compilador lida de forma diferente quando utilizamos a classe Expression<TDelegate>, o código IL gerado para ele corresponde à:
private static void Main(string[] args)
{
ParameterExpression CS$0$0000;
ParameterExpression CS$0$0001;
int resultado =
Expression.Lambda<Func<int, int, int>>(
Expression.Add(
CS$0$0000 = Expression.Parameter(typeof(int), “v1”)
, CS$0$0001 = Expression.Parameter(typeof(int), “v2”)
)
, new ParameterExpression[] { CS$0$0000, CS$0$0001 })
.Compile()(2, 3);
}
Como podemos ver, as expressões lambdas podem ser representadas como código (delegates) ou como dados (árvore de expressões). É importante lembrar que uma expressão é um tipo de Abstract Syntax Tree (AST), que é uma estrutura de dados que representa um código já analisado. Essa técnica nos dá a habilidade de converter/traduzir um determinado código em outro, como é o caso do LINQ to SQL, que transforma essas árvores de expressão em linguagem T-SQL.
WCF – Gerenciamento de Instâncias
O gerenciamento de instância é uma técnica que é utilizada pelo WCF ou qualquer outra tecnologia de computação distribuída que determina como e por quem as requisições dos clientes serão atendidas. A escolha do modo de gerenciamento de instâncias interfere diretamente na escalabilidade, performance e transações de um serviço/componente, além de termos algumas mudanças à nível de implementação de contrato, que precisamos nos atentar para garantir que o mesmo funcione sob o modelo de gerenciamento escolhido.
A finalidade do artigo consiste, basicamente, em mostrar cada uma das três técnicas disponíveis pelo WCF mas, também, abordando os seus respectivos benefícios e algumas técnicas que circundam esse processo e que, de alguma forma, estão ligadas e influenciam na escolha e/ou implementação. O artigo também abordará os cuidados que devemos ter na escolha e implementação de cada uma das técnicas fornecidas.
Em primeiro lugar, é importante dizer que a escolha do modo de gerenciamento de instância não compromete diretamente o desenho do contrato do serviço. A escolha do modo aplica-se à classe de implementação do serviço que, por sua vez, faz o uso (implementação) da referida Interface. A escolha do modo de gerenciamento deverá ser realizada a partir de um behavior* de serviço pré-definido dentro do .NET Framework como um atributo, chamado de ServiceBehaviorAttribute.
* Em uma resposta curta, um behavior customiza a execução do serviço. Para ser mais específico, um behavior consiste em um pedaço de código que implementa uma determinada Interface (dependendo de onde o mesmo será aplicado) que permite ao runtime do WCF adicioná-lo no processo de execução e, conseqüentemente, executar o código customizado por você. Eles podem ser “plugados” via atributos ou arquivo de configuração. A criação de behaviors customizados está fora do escopo deste artigo.
Esse atributo possui uma propriedade chamada InstanceContextMode que recebe uma das opções fornecidas pelo enumerador InstanceContextMode. Entre as opções fornecidas por ele temos: PerSession, PerCall e Single. São essas três opções que abordaremos extensivamente a partir de agora.
PerSession
O modo PerSession que, quando omitido é o modo utilizado pelo WCF, cria uma instância da classe que corresponde ao serviço para cada cliente. A ativação é realizada quando o primeiro método é invocado por ele e a instância ficará ativa, respondendo à todas as requisições subseqüentes até que o cliente feche a conexão, que tipicamente acontece quando o método Close do proxy (ClientBase) é invocado (lembrando que o método Dispose também invoca o método Close). Justamente pelo objeto ficar ativo entre a abertura e o fechamento, ele é tipicamente referido como Session do lado do cliente e como Private Session do lado do serviço.
Cada instância ativa do lado do serviço correspondente à uma instância ativa do proxy que está do lado do cliente, ou seja, se um único cliente criar cinco instâncias do mesmo proxy, mesmo que ele aponte para o mesmo serviço, será criada uma classe para cada um deles do lado do serviço, para atender as respectivas chamadas, não podendo uma Session ser compartilhada com outra, mesmo sendo oriunda do mesmo cliente.
Mesmo que o cliente crie várias threads, utilizando a mesma instância do proxy para fazer várias chamadas, o serviço conseguirá atender somente uma única thread e, sendo assim, as outras threads terão que aguardar a liberação para serem processadas. É possível mudarmos esse comportamento utilizando as técnicas de concorrências fornecidas pelo WCF, mas que estão fora do escopo deste artigo.
O trecho de código abaixo ilustra como podemos aplicar o atributo ServiceBehaviorAttribute na classe de implementação do serviço, definindo ele como sendo PerSession mas, se omitirmos o atributo, o comportamento será o mesmo pois, como dito acima, é a configuração padrão do WCF:
using System;
using System.ServiceModel;
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServidoDeClientes : IClientes
{
private Dictionary<int, string> _clientes;
//Construtor
public int Adicionar(string nome)
{
int key = this._clientes.Count + 1;
this._clientes.Add(key, nome);
return key;
}
}
|
Como podemos notar, a classe está definida como PerSession. Neste caso, como a instância ficará ativa enquanto durar o proxy, podemos armazenar os dados entre as requisições na própria classe, ou seja, em seus membros internos, como é o caso do objeto _clientes. Quando o proxy for encerrado, ele notificará o serviço que a sessão foi finalizada e, caso o mesmo implemente a Interface IDisposable, o método Dispose será invocado*.
* Como o método Dispose será invocado assíncronamente, quer dizer, em uma worker thread, o contexto da operação não será propagado para dentro do método Dispose e, sendo assim, o mesmo não pode depender destas informações contextuais.
Mas para que o suporte a sessão funcione corretamente, precisamos analisar sob qual binding a classe será exposta. Para que seja possível correlacionar as mensagens vindas de um determinado cliente para uma determinada instância, o WCF precisa ser capaz de identificar o cliente e, quando utilizamos bindings que fornecem isso nativamente através do protocolo (como é o caso do TCP ou IPC), não há problema, pois eles mantém uma conexão contínua, conhecida também como sessão à nível de transporte e, com isso, pode-se facilmente identificar quem é o cliente. Pela natureza do protocolo HTTP isso já é um pouco mais complexo, pois a cada chamada uma nova conexão é realizada e, com isso, o BasicHttpBiding não suporta Sessions; já o WSHttpBinding é capaz de emular a sessão à nível de transporte, incluindo um SessionID nos headers da mensagem que identifica unicamente o cliente, muito semelhante ao que acontece com o ASP.NET.
A SessionID é representada por uma GUID e tanto o cliente quanto o serviço são capazes de recuperá-la para que ela possa ser utilizada durante a execução, para alguma determinada tarefa, bem como para efeitos de depuração/logging. O WCF (assim como funcionava também no COM+) fornece uma classe contendo informações contextuais (falaremos mais sobre contexto abaixo), que estão relacionadas à chamada corrente que, além de trazer informações a respeito de transações e segurança, informa também a SessionID. Essa informação é acessada de forma diferente no cliente em relação ao servidor. O trecho de código abaixo ilustra como proceder para recuperar tal informação em ambos os lados:
//Serviço Debug.WriteLine(OperationContext.Current.SessionId); //Cliente Debug.WriteLine(proxy.InnerChannel.SessionId); |
Ainda há mais um tipo que precisamos nos atentar. Trata-se do enumerador SessionMode que pode ser atribuído à propriedade SessionMode do atributo ServiceContractAttribute que é aplicado na Interface de contrato do serviço. Essa informação será disponibilizada no contrato do serviço, para que o runtime do WCF saiba que ele deve utilizar uma Session. Esse enumerador fornece três opções, que estão descritas abaixo:
- Allowed: Especifica que o contrato suporte sessão desde que o binding também suporte. Por padrão, esta opção é definida nos contratos quando a mesma não estiver explícita.
- NotAllowed: Determina que o contrato não aceite bindings que iniciem sessões.
-
Required: Especifica que o contrato necessite de um binding que suporte sessões e, caso isso não aconteça, uma exceção do tipo InvalidOperationException será lançada durante a carga do serviço.
Como pudemos notar, a finalidade deste enumerador é garantir que o contrato será exposto a partir de um binding que suporte (ou não) as Sessions. A configuração padrão determina a propriedade SessionMode para Allowed e, com isso, se estiver utilizando algum binding que não suporte sessões, como é o caso do BasicHttpBinding, nenhuma exceção será lançada, mas ele não se comportará como o esperado, ou seja, não conseguirá manter o estado entre as chamadas.
Já quando escolhemos a opção NotAllowed e disponibilizamos o contrato a partir de um binding que, nativamente, traz suporte a sessões, como é o caso do TCP, então uma exceção será lançada durante a carga do serviço. Finalmente, a opção Required obriga que o contrato seja exposto sob um binding que suporte sessões e, caso não suporte, assim como a opção anterior, uma exceção será lançada durante a carga do serviço. O trecho de código abaixo ilustra como definir essa propriedade no contrato do serviço:
using System;
using System.ServiceModel;
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ICliente
{
[OperationContract]
int Adicionar(string nome);
}
|
Ordem de Execução
Quando utilizamos contratos que suportam sessão, às vezes precisamos determinar a ordem de execução, garatindo assim que um método seja capaz de inicializar a sessão; já outro método poderá somente ser executado caso a sessão já esteja devidamente criada, e ainda há a possibilidade de um método encerrar a sessão.
Para termos um controle sobre isso, utilizamos duas propriedades chamadas IsInitiating e IsTerminating do atributo OperationContractAttribute. Essas propriedades irão demarcar o limite da sessão dentro do serviço e uma consistência é realizada durante a carga do mesmo para garantir que ele esteja com a opção SessionMode definida como Required. Por padrão, a propriedade IsInitiating é definida como True e IsTerminating como False, permitindo que qualquer método inicie a sessão. O interessante é que esta técnica evitará que você faça consistências dentro do método para saber se a sessão já está ou não ativa, pois o próprio WCF garante isso, uma vez que você está tentando acessar um método que não permite inicializar a sessão e a mesma ainda não esteja criada.
Para exemplificar, vamos imaginar que temos um contrato que define algumas operações típicas para um comércio eletrônico. As operações consistem em identificar o cliente a partir de um login, possibilidade de adicionar produtos, recuperar o valor total dos produtos e, finalmente, um método para finalizar a compra. O código abaixo ilustra como devemos fazer para configurar esses métodos dentro da Interface para garantir a ordem das chamadas:
using System;
using System.ServiceModel;
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IComercioEletronico
{
[OperationContract(IsInitiating = true)]
bool EfetuarLogin(string nome);
[OperationContract(IsInitiating = false)]
void AdicionarProduto(int codigoDoProduto, int quantidade);
[OperationContract(IsInitiating = false)]
decimal RecuperarValorTotal();
[OperationContract(IsInitiating = false, IsTerminating = true)]
void FinalizarCompra();
}
|
Apesar da propriedade IsInitiating ser definida, por padrão, como True, não seria necessário especificá-la no método EfetuarLogin; já os dois métodos intermediários, AdicionarProduto e RecuperarValorTotal não permitem inicializar ou finalizar a sessão (lembre-se de que a propriedade IsTerminating por padrão é sempre False) e, finalmente, o método FinalizarCompra encerra a sessão, pois define IsInitiating como False e IsTerminating como True.
Com este contrato devidamente implementado podemos, do lado do cliente, criar a referência para o serviço, instanciar o proxy e consumir os métodos. Caso você tente invocar um método que não crie a sessão antes de invocar o método que tem essa finalidade, você receberá uma exceção do tipo InvalidOperationException, informando que o método que está tentando invocar somente poderá ser disparado depois da sessão criada. O trecho de código abaixo mostra como invocar os métodos em sua seqüência correta (repare que isso não muda em nada o código cliente):
using (ComercioEletronicoClient proxy = new ComercioEletronicoClient())
{
proxy.EfetuarLogin("Israel Aece");
proxy.AdicionarProduto(12, 4);
proxy.AdicionarProduto(12, 3);
proxy.AdicionarProduto(3, 3);
Console.WriteLine(proxy.RecuperarValorTotal());
proxy.FinalizarCompra();
}
|
Se tentar chamar qualquer método antes do método EfetuarLogin, uma exceção será lançada, dizendo que isso não é possível, obrigando você a invocar os métodos na ordem correta. Além disso, é importante dizer que como o método FinalizarCompra está com a propriedade IsTerminating definida como True, ao invocá-lo, o WCF removerá a instância corrente, invalidando o proxy. Caso você necessite fazer novas chamadas, você terá que criar um novo proxy.
Apesar desta técnica estar sendo abordada dentro da seção que discute o modo PerSession, ela pode também ser adotada em um serviço que é exposto através do modo Single, que será abordado mais abaixo.
Desativação da Instância
O modelo PerSession permite que várias mensagens estejam correlacionadas à uma mesma instância, mantendo o objeto ativo do lado do serviço, atendendo às requisições de um determinado proxy. Na verdade as instâncias estão contidas dentro de um contexto e, quando uma sessão é criada, o host cria um novo contexto para acomodar a instância dentro dele. A imagem abaixo ilustra graficamente como esse processo acontece:
|
|
| Figura 1 – A relação entre o contexto e a instância do serviço. |
Como podemos notar, a instância sempre estará contida dentro de um contexto que, por sua vez, tem o mesmo tempo de vida do objeto. Entretanto, em um ambiente onde você quer ter um maior controle em relação ao gerenciamento dos contextos e também das instâncias, talvez por questões de performance ou mesmo de consistência, o WCF desacopla o gerenciamento da instância em relação ao contexto onde ela reside e, com isso, você poderá definir quando descartar a instância (antes ou depois da chamada do método), mas tendo ainda o acesso ao contexto corrente, algo que não era possível antes. Esse controle é útil em momentos em que o método utiliza recursos valiosos, onde não se pode esperar até que a sessão finalize para que eles sejam liberados.
Esse controle pode ser realizado de forma imperativa ou declarativa. Isso quer dizer que podemos, no interior de um método, invocar um método do WCF que libera a instância ou, da forma declarativa, onde podemos decorar os métodos que fazem parte do serviço a descartar a instância antes ou depois da execução do mesmo. Para a primeira forma, recorremos ao método ReleaseServiceInstance da classe InstaceContext, como mostra o exemplo de código abaixo:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
//Outros métodos
public void FinalizarCompra()
{
//finalizar a compra
OperationContext.Current.InstanceContext.ReleaseServiceInstance();
}
}
|
Já no modelo declarativo, recorremos ao atributo OperationBehaviorAttribute. Este atributo fornece uma propriedade chamada ReleaseInstance que aceita os dois itens contidos dentro do enumerador ReleaseInstanceMode, que dizem ao WCF que a instância deve ser reciclada em um ponto específico do processo. As opções deste enumerador são abordadas abaixo:
- None: É a opção padrão que recicla o objeto de acordo com o valor definido na propriedade InstanceContextMode (PerSession, PerCall e Single).
- BeforeCall: Recicla o objeto antes da chamada da operação.
- AfterCall: Recicla o objeto depois da chamada da operação. Semelhante ao método ReleaseServiceInstance.
- BeforeAndAfterCall: Recicla o objeto antes e depois da chamada da operação. Esta opção é muito semelhante aos serviços configurados como PerCall.
Abaixo o mesmo exemplo que vimos acima, só que agora no modo declarativo. É importante notar que em ambos os casos essas configurações são realizadas na classe que implementa o contrato:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
//Outros métodos
[OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.BeforeCall)]
public void FinalizarCompra()
{
//finalizar a compra
}
}
|
Determinar qual das três opções de sessão escolher (Allowed, NotAllowed ou Required), dependerá muito do que o teu contrato se propõe a realizar. Serviços configurados como PerSession tem um alto custo e não escalam bem em relação aos serviços configurados como PerCall. Isso acontece porque enquanto o proxy permanecer ativo do lado do cliente, a classe correspondente que serve as suas requisições também ficará ativa do lado do serviço. Caso as chamadas forem feitas com bastante freqüência, manter o proxy pode ser uma boa solução; já quando as chamadas forem esporádicas ou se precisar de escalabilidade ou consistência, então o melhor é recorrer ao modo PerCall, que será abordado logo abaixo.
PerCall
Enquanto a característica de serviços configurados como PerSession é manter o objeto ativo do lado do serviço enquanto existir o proxy do lado do cliente, o PerCall é o oposto. Muitas vezes os clientes prendem a referência para o objeto por um longo período de tempo e, na verdade, fazem o uso dele em poucos momentos. Isso fará com que os objetos consumam memória desnecessária do lado do serviço, sem ao menos ser utilizada.
Serviços configurados como PerCall criarão uma nova instância do serviço a cada chamada para qualquer operação. Quando o método retornar, o objeto será removido da memória, existindo apenas durante a execução do método solicitado pelo cliente. Para classes que implementam a Interface IDisposable, momentos antes da classe ser removida, o método Dispose será disparado mas, ao contrário do modelo PerSession, o método Dispose será executado na mesma thread da operação e, conseqüentemente, você ainda terá acesso ao contexto.
Neste modo de gerenciamento, você precisa se atentar à alguns detalhes que mudam consideravelmente em relação ao modo PerSession. O primeiro deles é com relação a concorrência; como cada chamada criará uma nova instância dedicada, então você não precisará lidar com ambiente multi-threading, pois ele nunca irá acontecer. Outro detalhe importante é com relação ao gerenciamento de estado. Como a instância que servirá a requisição será destruída quando o método retornar, não será possível que você guarde alguma informação entre as requisições e, caso queria, você precisará criar e gerenciar um repositório próprio, além do que precisará também identificar unicamente o cliente a cada chamada e, na maioria das vezes, isso influencia em uma mudança no contrato do serviço.
Seguindo o mesmo exemplo que utilizamos no modo PerSession, mudaremos ligeiramente a classe ServidoDeClientes para, neste momento, utilizar o modelo PerCall. Tudo o que precisamos é definir a propriedade InstanceContextMode do atributo ServiceBehaviorAttribute para PerCall e tudo já funcionará com este novo comportamento:
using System;
using System.ServiceModel;
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class ServidoDeClientes : IClientes
{
private Dictionary<int, string> _clientes;
//Construtor
public int Adicionar(string nome)
{
int key = this._clientes.Count + 1;
this._clientes.Add(key, nome);
return key;
}
}
|
Como já é de se esperar, a cada chamada para o método Adicionar, ele sempre criará uma nova instância da classe e, conseqüentemente, todo o conteúdo será recriado, não mantendo as informações entre as solicitações. Como dito acima, caso você precise manter alguma informação entre as requisições, você precisará inicializar essas informações a partir de algum repositório, como banco de dados, sistema de arquivos ou até mesmo variáveis estáticas.
Single
Já no modelo Single (Singleton) o comportamento é muito diferente em relação às duas opções que vimos acima. A idéia deste modelo é ter uma única instância servindo à todas as requisições que chegarem para este serviço, independente de qual endpoint venha a requisição. Este objeto viverá enquanto o host que a hospeda viver.
Como uma das principais vantagens, o modo Single não cria para todo cliente ou para toda chamada uma nova instância do objeto mas, em contrapartida, há algumas desvantagens que são um pouco difícies de lidar. O primeiro caso é com relação à concorrência: como você tem múltiplos clientes acessando a mesma instância, objetos contidos dentro dela também poderão ser acessados ao mesmo tempo e, se não se preocupar com a forma de acessá-los, poderá ter problemas no resultado final do processo ou até mesmo deadlocks. Outro problema conhecido mas que não se pode fazer muito a respeito, justamente pela necessidade de ter um único objeto fornecendo todas as requisições, é que ela (instância) ficará ativa mesmo quando não há nenhuma tarefa para ela executar e, dependendo da quantidade de recursos que a mesma armazena, poderemos ter problemas futuros.
Para utilizar o modo Single, assim como os outros, devemos recorrer ao atributo ServiceBehaviorAttribute, definindo a propriedade InstanceContextMode como Single. Mas é importante ressaltar que, para membros internos, será necessário que você entenda os possíveis problemas que podem acontecer pelo fato de múltiplos clientes fazerem requisições simultâneas, e isso exige uma mudança na forma de programar e/ou acessar os recursos internos da classe. A concorrência não é tratada aqui, pois precisa de um artigo a parte para abordar isso. O código abaixo ilustra o mesmo exemplo (ServidoDeClientes), mas utilizando o modo Single:
using System;
using System.ServiceModel;
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ServidoDeClientes : IClientes
{
private Dictionary<int, string> _clientes;
//Construtor
public int Adicionar(string nome)
{
int key = this._clientes.Count + 1;
this._clientes.Add(key, nome);
return key;
}
}
|
A classe ServiceHost, que é responsável pelo gerenciamento da instância da classe de serviço, possui alguns overloads do construtor que, em uma das versões, aceita uma instância do tipo (Type) do serviço; neste caso o ServiceHost é responsável por instanciar a classe, invocando o construtor padrão da mesma. Já em outra versão do construtor do ServiceHost, ele recebe um parâmetro do tipo object, permitindo que você instancie a classe de implementação do serviço. Isso é útil quando a implementação do serviço necessita de algumas informações adicionais (via construtor ou propriedades) para poder trabalhar. Abaixo há um exemplo que mostra essa técnica:
ServicoDeClientes srv = new ServicoDeClientes("connection_string");
srv.NumeroDeTentativas = 5;
using(ServiceHost host = new ServiceHost(srv,
new Uri[] { new Uri("net.tcp://localhost:9292") }))
{
//...
}
|
Conclusão: Este artigo mostrou as três formas que temos para gerenciar a instância de uma classe que serve as requisições. É importante analisar as vantagens e desvantagens de cada uma delas para decidir qual delas é a ideal para o serviço que está sendo desenvolvido. Além disso, a escolha do modo de gerenciamento interfere diretamente no modo de escrita de código, justamente para possibilitar que o serviço se comporte bem em um ambiente multi-threading, já que os modos PerSession e Single permitem que isso aconteça.
