Объединение в пулы

В примере пулирования показано, как расширить Windows Communication Foundation (WCF) для поддержки пула объектов. Этот образец демонстрирует, как создать атрибут, синтаксически и семантически аналогичный функциям атрибута ObjectPoolingAttribute служб Enterprise Services. Использование пулов объектов может значительно повысить производительность приложения. Однако при неправильном использовании эффект может быть противоположным. Использование пулов объектов позволяет снизить накладные расходы на повторное создание часто используемых объектов, требующих большого объема инициализации. Однако если завершение вызова метода для объекта из пула занимает много времени, сразу после достижения максимального размера пула функция пулов объектов ставит дополнительные запросы в очередь. В результате возможен сбой обслуживания запросов создания некоторых объектов из-за возникновения исключения времени ожидания.

Примечание.

Процедура настройки и инструкции по построению для данного образца приведены в конце этого раздела.

Первым шагом при создании расширения WCF является решение точки расширяемости, используемой.

В WCF диспетчер терминов относится к компоненту времени выполнения, ответственному за преобразование входящих сообщений в вызовы методов в службе пользователя и преобразование возвращаемых значений из этого метода в исходящее сообщение. Служба WCF создает диспетчер для каждой конечной точки. Клиент WCF должен использовать диспетчер, если контракт, связанный с этим клиентом, является дуплексным контрактом.

Диспетчеры каналов и конечных точек обеспечивают расширяемость на уровне канала и контракта, предоставляя различные свойства, которые управляют поведением диспетчера. Свойство DispatchRuntime также позволяет контролировать, изменять или настраивать диспетчерский процесс. В этом образце рассматривается свойство InstanceProvider, которое указывает на объект, предоставляющий экземпляры класса службы.

IInstanceProvider

В WCF диспетчер создает экземпляры класса службы с помощью объекта InstanceProvider, реализующего IInstanceProvider интерфейс. У этого интерфейса есть три метода.

Пул объектов

Пользовательская реализация IInstanceProvider обеспечивает необходимую семантику пула объектов для службы. Поэтому в образце имеется тип ObjectPoolingInstanceProvider, который предоставляет пользовательскую реализацию интерфейса IInstanceProvider для создания пула. Когда Dispatcher вызывает метод GetInstance(InstanceContext, Message), пользовательская реализация вместо создания нового экземпляра ищет существующий объект в находящемся в памяти пуле. Если такой объект доступен, метод возвращает его. В противном случае создается новый объект. Реализация метода GetInstance показана в следующем образце кода.

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;
}

Пользовательская реализация ReleaseInstance добавляет освободившийся экземпляр обратно в пул и уменьшает значение ActiveObjectsCount на единицу. Dispatcher может вызывать эти методы из различных потоков, поэтому требуется синхронизированный доступ к членам уровня класса в классе ObjectPoolingInstanceProvider.

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();
    }
}

Этот ReleaseInstance метод предоставляет функцию "очистки инициализации". Обычно пул поддерживает минимальное число объектов в течение времени существования пула. Однако возможны периоды интенсивного использования, когда требуется создавать в пуле дополнительные объекты, пока не будет достигнуто заданное в конфигурации максимальное значение. В конце концов, когда активность пула снизится, эти дополнительные объекты могут стать излишней нагрузкой. Поэтому когда значение activeObjectsCount достигает нуля, запускается таймер бездействия, по истечении времени ожидания которого выполняется цикл очистки.

Добавление поведения

Расширения уровня диспетчера подключаются с помощью следующих поведений.

  • Поведения служб. Позволяют настраивать всю среду выполнения службы.

  • Поведения конечных точек. Позволяют настраивать конечные точки службы, включая диспетчера каналов и конечных точек.

  • Поведения контрактов. Эти поведения позволяют настраивать классы ClientRuntime и DispatchRuntime в клиенте и службе соответственно.

С целью реализации расширения создания пулов объектов должно быть создано поведение службы. Поведения служб создаются путем реализации интерфейса IServiceBehavior. Имеется несколько способов сообщить модели службы о пользовательских поведениях:

  • с помощью пользовательского атрибута;

  • Императивно добавляя его в коллекцию поведений описания службы.

  • путем расширения файла конфигурации.

В этом образце используется пользовательский атрибут. ServiceHost При построении он проверяет атрибуты, используемые в определении типа службы, и добавляет доступные поведения в коллекцию поведений службы.

У интерфейса IServiceBehavior имеется три метода — Validate, AddBindingParameters и ApplyDispatchBehavior. Метод Validate используется для обеспечения того, что поведение может быть применено к службе. В этом образце реализация обеспечивает, что служба не настраивается с Single. Метод AddBindingParameters используется для настройки привязок службы. Это не требуется в данном сценарии. Метод ApplyDispatchBehavior используется для настройки диспетчеров службы. Этот метод вызывается WCF при ServiceHost инициализации. Этому методу передаются следующие параметры.

  • Description: этот аргумент содержит описание службы для всей службы. Это можно использовать для проверки данных описания конечных точек службы, контрактов, привязок и других данных.

  • ServiceHostBase: этот аргумент содержит инициализируемый в данный момент объект ServiceHostBase.

В пользовательской реализации IServiceBehavior создается новый экземпляр ObjectPoolingInstanceProvider, который присваивается свойству InstanceProvider в каждом объекте DispatchRuntime в 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;
     }
}

Помимо реализации интерфейса IServiceBehavior у класса ObjectPoolingAttribute имеется несколько членов для настройки пула объектов с помощью аргументов атрибута. К этим членам относятся MaxPoolSize, MinPoolSize и CreationTimeout, и они должны соответствовать набору возможностей пула, предоставляемому службами .NET Enterprise Services.

Теперь поведение пула объектов можно добавить в службу WCF, заметив реализацию службы с новым пользовательским ObjectPooling атрибутом.

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

Запуск примера

В этом образце демонстрируются преимущества в производительности, которые могут быть получены при использовании пула объектов в различных сценариях.

Приложение службы реализует две службы - WorkService и ObjectPooledWorkService. Обе службы совместно используют одну реализацию - обеим требуется обширная инициализация, а затем обе предоставляют метод DoWork(), требующий относительно малых затрат. Единственное отличие заключается в том, что в службе 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.");
    }
}

При выполнении клиента он измеряет время 5-кратного вызова службы WorkService. Затем измеряется время 5-кратного вызова службы ObjectPooledWorkService. Затем отображается разница во времени:

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.

Примечание.

При первом запуске клиента обращение к обеим службам занимает приблизительно одинаковое время. При повторном запуске образца видно, что служба ObjectPooledWorkService возвращает результаты намного быстрее, потому что экземпляр этого объекта уже существует в пуле.

Настройка, сборка и выполнение образца

  1. Убедитесь, что вы выполнили процедуру однократной установки для примеров Windows Communication Foundation.

  2. Чтобы создать решение, следуйте инструкциям по созданию примеров Windows Communication Foundation.

  3. Чтобы запустить пример в конфигурации с одним или несколькими компьютерами, следуйте инструкциям в разделе "Примеры Windows Communication Foundation".

Примечание.

Если для восстановления конфигурации этого образца используется программа Svcutil.exe, измените имя конечной точки в конфигурации клиента, чтобы оно соответствовало клиентскому коду.