Agrupamento

A amostra de pooling demonstra como estender o WCF (Windows Communication Foundation) para dar suporte ao pool de objetos. A amostra demonstra como criar um atributo que seja sintaticamente e semanticamente semelhante à funcionalidade de atributo ObjectPoolingAttribute dos Serviços Enterprise. O pool de objetos pode fornecer um aumento dramático no desempenho de um aplicativo. No entanto, ele poderá ter o efeito oposto se não for usado corretamente. O pool de objetos ajuda a reduzir a sobrecarga da recriação de objetos usados com frequência que exigem uma inicialização extensiva. No entanto, se uma chamada a um método em um objeto em pool demorar um tempo considerável para ser concluída, o pool de objetos agrupará solicitações adicionais assim que o tamanho máximo do pool for atingido. Portanto, ele pode falhar ao atender algumas solicitações de criação de objeto lançando uma exceção de tempo limite.

Observação

O procedimento de instalação e as instruções de compilação dessa amostra estão no final deste tópico.

A primeira etapa na criação de uma extensão WCF é decidir o ponto de extensibilidade a ser usado.

No WCF, o termo dispatcher refere-se a um componente em tempo de execução responsável por converter mensagens de entrada em invocações de método no serviço do usuário e por converter valores retornados desse método em uma mensagem de saída. Um serviço WCF cria um dispatcher para cada ponto de extremidade. Um cliente WCF deve usar um dispatcher se o contrato associado a ele for duplex.

Os dispatchers de canal e ponto de extremidade oferecem extensibilidade de canal e contrato expondo diversas propriedades que controlam o comportamento do dispatcher. A propriedade DispatchRuntime também permite inspecionar, modificar ou personalizar o processo de expedição. Esta amostra se concentra na propriedade InstanceProvider que aponta para o objeto que fornece as instâncias da classe de serviço.

O IInstanceProvider

No WCF, o dispatcher cria instâncias da classe de serviço usando um InstanceProvider, que implementa a interface IInstanceProvider. Essa interface possui três métodos:

O pool de objetos

Uma implementação personalizada IInstanceProvider fornece a semântica de pool de objetos necessária para um serviço. Portanto, este exemplo tem um tipo ObjectPoolingInstanceProvider que fornece implementação personalizada de IInstanceProvider para pooling. Quando Dispatcher chama o método GetInstance(InstanceContext, Message), em vez de criar uma nova instância, a implementação personalizada procura um objeto existente em um pool na memória. Se um estiver disponível, ele será retornado. Caso contrário, um novo objeto será criado. A implementação de GetInstance é mostrada no código de exemplo a seguir.

object IInstanceProvider.GetInstance(InstanceContext instanceContext, Message message)
{
    object obj = null;

    lock (poolLock)
    {
        if (pool.Count > 0)
        {
            obj = pool.Pop();
        }
        else
        {
            obj = CreateNewPoolObject();
        }
        activeObjectsCount++;
    }

    WritePoolMessage(ResourceHelper.GetString("MsgNewObject"));

    idleTimer.Stop();

    return obj;
}

A implementação personalizada ReleaseInstance adiciona a instância liberada de volta ao pool e diminui o valor ActiveObjectsCount. Dispatcher pode chamar esses métodos de threads diferentes e, portanto, o acesso sincronizado aos membros de nível de classe na classe ObjectPoolingInstanceProvider é necessário.

void IInstanceProvider.ReleaseInstance(InstanceContext instanceContext, object instance)
{
    lock (poolLock)
    {
        pool.Push(instance);
        activeObjectsCount--;

        WritePoolMessage(
        ResourceHelper.GetString("MsgObjectPooled"));

        // When the service goes completely idle (no requests
        // are being processed), the idle timer is started
        if (activeObjectsCount == 0)
            idleTimer.Start();
    }
}

O método ReleaseInstance fornece um recurso de "inicialização de limpeza". Normalmente, o pool mantém um número mínimo de objetos para o tempo de vida do pool. No entanto, pode haver períodos de uso excessivo que exigem a criação de objetos adicionais no pool para atingir o limite máximo especificado na configuração. Eventualmente, quando o pool se torna menos ativo, esses objetos excedentes podem se tornar uma sobrecarga extra. Portanto, quando activeObjectsCount chega a zero, um temporizador ocioso é iniciado que dispara e executa um ciclo de limpeza.

Adicionando o comportamento

As extensões da camada do dispatcher são conectadas usando os seguintes comportamentos:

  • Comportamentos de serviço. Elas permitem a personalização de todo o runtime do serviço.

  • Comportamentos de Ponto de Extremidade. Elas permitem a personalização dos pontos de extremidade de serviço, especificamente um Dispatcher de Canal e Ponto de Extremidade.

  • Comportamentos do contrato. Elas permitem a personalização das classes ClientRuntime e DispatchRuntime no cliente e no serviço, respectivamente.

Para fins de uma extensão de pool de objetos, um comportamento de serviço deve ser criado. Os comportamentos de serviço são criados pela implementação da interface IServiceBehavior. Há várias maneiras de tornar o modelo de serviço ciente dos comportamentos personalizados:

  • Usando um atributo personalizado.

  • Adicionando-o imperativamente à coleção de comportamentos da descrição do serviço.

  • Estendendo o arquivo de configuração.

Esta amostra usa um atributo personalizado. Quando ServiceHost for construído, ele examinará os atributos usados na definição de tipo do serviço e adicionará os comportamentos disponíveis à coleção de comportamentos da descrição do serviço.

A interface IServiceBehavior tem três métodos -- Validate, AddBindingParameters e ApplyDispatchBehavior. O método Validate é usado para garantir que o comportamento possa ser aplicado ao serviço. Neste exemplo, a implementação garante que o serviço não esteja configurado com Single. O método AddBindingParameters é usado para configurar as associações do serviço. Isso não é necessário neste cenário. O recurso ApplyDispatchBehavior é usado para configurar os dispatchers do serviço. Esse método é chamado pelo WCF quando ServiceHost está sendo inicializado. Os seguintes parâmetros são passados para este método:

  • Description: esse argumento fornece a descrição do serviço para todo o serviço. Isso pode ser usado para inspecionar dados de descrição sobre pontos de extremidade, contratos, associações e outros dados do serviço.

  • ServiceHostBase: esse argumento fornece o ServiceHostBase que está sendo inicializado no momento.

Na implementação personalizada IServiceBehavior, uma nova instância ObjectPoolingInstanceProvider é criada e atribuída à propriedade InstanceProvider em cada DispatchRuntime no ServiceHostBase.

void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
    // Create an instance of the ObjectPoolInstanceProvider.
    ObjectPoolingInstanceProvider instanceProvider = new
           ObjectPoolingInstanceProvider(description.ServiceType,
                                                    minPoolSize);

    // Forward the call if we created a ServiceThrottlingBehavior.
    if (this.throttlingBehavior != null)
    {
        ((IServiceBehavior)this.throttlingBehavior).ApplyDispatchBehavior(description, serviceHostBase);
    }

    // In case there was already a ServiceThrottlingBehavior
    // (this.throttlingBehavior==null), it should have initialized
    // a single ServiceThrottle on all ChannelDispatchers.
    // As we loop through the ChannelDispatchers, we verify that
    // and modify the ServiceThrottle to guard MaxPoolSize.
    ServiceThrottle throttle = null;

    foreach (ChannelDispatcherBase cdb in
            serviceHostBase.ChannelDispatchers)
    {
        ChannelDispatcher cd = cdb as ChannelDispatcher;
        if (cd != null)
        {
            // Make sure there is exactly one throttle used by all
            // endpoints. If there were others, we could not enforce
            // MaxPoolSize.
            if ((this.throttlingBehavior == null) &&
                        (this.maxPoolSize != Int32.MaxValue))
            {
                throttle ??= cd.ServiceThrottle;
                if (cd.ServiceThrottle == null)
                {
                    throw new
InvalidOperationException(ResourceHelper.GetString("ExNullThrottle"));
                }
                if (throttle != cd.ServiceThrottle)
                {
                    throw new InvalidOperationException(ResourceHelper.GetString("ExDifferentThrottle"));
                }
             }

             foreach (EndpointDispatcher ed in cd.Endpoints)
             {
                 // Assign it to DispatchBehavior in each endpoint.
                 ed.DispatchRuntime.InstanceProvider =
                                      instanceProvider;
             }
         }
     }

     // Set the MaxConcurrentInstances to limit the number of items
     // that will ever be requested from the pool.
     if ((throttle != null) && (throttle.MaxConcurrentInstances >
                                      this.maxPoolSize))
     {
         throttle.MaxConcurrentInstances = this.maxPoolSize;
     }
}

Além de uma implementação IServiceBehavior, a classe ObjectPoolingAttribute tem vários membros para personalizar o pool de objetos usando os argumentos de atributo. Esses membros incluem MaxPoolSize, MinPoolSize e CreationTimeout, para corresponder ao conjunto de recursos de pool de objetos fornecido pelo .NET Enterprise Services.

O comportamento de pool de objetos agora pode ser adicionado a um serviço WCF anotando a implementação do serviço com o atributo personalizado ObjectPooling recém-criado.

[ObjectPooling(MaxPoolSize=1024, MinPoolSize=10, CreationTimeout=30000)]
public class PoolService : IPoolService
{
  // …
}

Executando o exemplo

O exemplo demonstra os benefícios de desempenho que podem ser obtidos usando o pool de objetos em determinados cenários.

O aplicativo de serviço implementa dois serviços -- WorkService e ObjectPooledWorkService. Ambos os serviços compartilham a mesma implementação -- ambos exigem inicialização cara e expõem um método DoWork() relativamente barato. A única diferença é que o pool de objetos está configurado no ObjectPooledWorkService:

[ObjectPooling(MinPoolSize = 0, MaxPoolSize = 5)]
public class ObjectPooledWorkService : IDoWork
{
    public ObjectPooledWorkService()
    {
        Thread.Sleep(5000);
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService instance created.");
    }

    public void DoWork()
    {
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService.GetData() completed.");
    }
}

Quando você executa o cliente, ele liga o WorkService 5 vezes. Em seguida, ele liga o ObjectPooledWorkService 5 vezes. A diferença de tempo é exibida em seguida:

Press <ENTER> to start the client.

Calling WorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling WorkService took: 26722 ms.
Calling ObjectPooledWorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling ObjectPooledWorkService took: 5323 ms.
Press <ENTER> to exit.

Observação

Na primeira vez que o cliente é executado, ambos os serviços parecem levar aproximadamente a mesma quantidade de tempo. Se você executar novamente a amostra, poderá ver que os retornos ObjectPooledWorkService são muito mais rápidos porque uma instância desse objeto já existe no pool.

Para configurar, compilar, e executar o exemplo

  1. Verifique se você executou o Procedimento de instalação única para os exemplos do Windows Communication Foundation.

  2. Para compilar a solução, siga as instruções contidas em Como compilar as amostras do Windows Communication Foundation.

  3. Para executar o exemplo em uma configuração de computador único ou cruzado, siga as instruções em Como executar os exemplos do Windows Communication Foundation.

Observação

Se você usar Svcutil.exe para regenerar a configuração desta amostra, modifique o nome do ponto de extremidade na configuração do cliente para corresponder ao código do cliente.