Улучшение производительности с помощью контроллеров

121

Инфраструктура MVC Framework предлагает два специальных вида контроллеров которые могут улучшить производительность разрабатываемых веб-приложений MVC. Как и в случае любой оптимизации производительности, при работе с этими контроллерами придется либо принести в жертву простоту использования, либо иметь дело с ограниченной функциональностью. В последующих разделах будет продемонстрировано применение обоих видов контроллеров и даны объяснения связанных с ними преимуществ и недостатков.

Использование контроллеров, не поддерживающих состояние сеанса

По умолчанию контроллеры поддерживают состояние сеанса, которое может использоваться для хранения значений данных между запросами, упрощая решение задач программистам MVC. Создание и поддержка состояния сеанса - сложный процесс. Данные должны сохраняться и извлекаться, а сами сеансы - управляться для обеспечения надлежащей политики устаревания. Данные сеанса расходуют память на сервере или в каком-то другом хранилище, и потребность в синхронизации данных между множеством веб-серверов усложняет запуск приложения на нескольких серверах (для крупных приложений).

Для упрощения поддержки состояния сеанса ASP.NET будет обрабатывать только один запрос для заданного сеанса за раз. Если клиент выдает множество перекрывающихся во времени запросов, сервер будет ставить их в очередь и обрабатывать последовательно. Преимущество такого подхода в том, что вам не потребуется переживать по поводу модификации одних и тех же данных многими запросами одновременно. Недостаток связан с невозможностью достижения необходимой пропускной способности запросов.

Не все контроллеры нуждаются в средстве состояния сеанса. В таких случаях производительность приложения можно улучшить, избежав работы, которая связана с поддержкой состояния сеанса. Это делается за счет применения контроллеров, не поддерживающих состояние сеанса. Они очень похожи на обычные контроллеры, но с двумя исключениями: инфраструктура MVC Framework не будет загружать или сохранять состояние сеанса, когда такие контроллеры используются для обработки запроса, и перекрывающиеся запросы могут быть обработаны одновременно.

Управление состоянием сеанса в специальной фабрике IControllerFactory

При обсуждении фабрики контроллеров в одной из предыдущих статей мы показали, что интерфейс IControllerFactory содержит метод по имени GetControllerSessionBehavior(), который возвращает значение перечисления SessionStateBehavior. Это перечисление содержит четыре значения, которые управляют конфигурацией состояния сеанса в контроллере:

Значения перечисления SessionStateBehavior
Значение Описание
Default

Используется стандартное поведение ASP.NET, при котором конфигурация состояния сеанса определяется из HttpContext

Required

Включено состояние сеанса, предназначенное для чтения-записи

Readonly

Включено состояние сеанса, предназначенное только для чтения

Disabled

Состояние сеанса полностью отключено

Фабрика контроллеров, которая реализует интерфейс IControllerFactory напрямую, устанавливает поведение состояния сеанса для контроллеров, возвращая значения SessionStateBehavior из метода GetControllerSessionBehavior(). Этот метод принимает в качестве параметров объект RequestContext и строку, содержащую имя контроллера. Можно возвращать любое из четырех значений, описанных в таблице выше и разные контроллеры могут возвращать разные значения.

В примере ниже приведена измененная реализация метода GetControllerSessionBehavior() из класса CustomControllerFactory, который был создан ранее:

// ...
public SessionStateBehavior GetControllerSessionBehavior(RequestContext
    requestContext, string controllerName)
{
    switch (controllerName)
    {
        case "Home":
            return SessionStateBehavior.ReadOnly;
        case "product":
            return SessionStateBehavior.Required;
        default:
            return SessionStateBehavior.Default;
    }
}
// ...

Управление состоянием сеанса с использованием DefaultControllerFactory

В случае использования встроенной фабрики контроллеров состоянием сеанса контроллера можно управлять за счет применения атрибута SessionState к индивидуальным классам контроллеров, как показано в примере ниже, где создан новый контроллер по имени FastController:

using System.Web.Mvc;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    [SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
    public class FastController : Controller
    {
        public ActionResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Fast ",
                ActionName = "Index"
            });
        }
    }
}

Атрибут SessionState применяется к классу контроллера и оказывает влияние на все методы действия в этом контроллере. Единственный параметр, принимаемый этим атрибутом - это значение перечисления SessionStateBehavior. В приведенном примере состояние сеанса полностью отключено. Это означает, что если попытаться установить значение сеанса в контроллере, например:

Session["key"] = "Привет";

или попробовать прочитать его в представлении:

Сообщение из состояния сеанса: @Session["key"]

то MVC Framework сгенерирует исключение, когда действие будет вызвано или представление визуализировано. Когда состоянием сеанса является Disabled, свойство HttpContext.Session возвращает null.

В случае если указано поведение Readonly, можно читать значения, установленные другими контроллерами, однако попытка установки или модификации значения по-прежнему будет приводить к исключению во время выполнения. Получить детальные сведения о сеансе можно с помощью объекта HttpContext.Session, но попытка изменить любое значение вызовет ошибку.

Если вам нужно просто передавать данные из контроллера в представление, подумайте об использовании вместо состояния сеанса объекта ViewBag. Атрибут SessionState на него влияния не оказывает.

Использование асинхронных контроллеров

Лежащая в основе MVC Framework платформа ASP.NET поддерживает пул потоков .NET, который используется для обработки клиентских запросов. Этот пул называется пулом рабочих потоков, а все потоки - соответственно, рабочими потоками. При получении запроса рабочий поток извлекается из пула и начинает обрабатывать запрос. После обработки запроса рабочий поток возвращается в пул, так что он будет доступен для обработки новых запросов по мере их поступления.

Применение пула потоков в приложениях ASP.NET обеспечивает два преимущества:

Пул рабочих потоков функционирует лучше, когда запросы могут быть обработаны в течение короткого периода времени. Такая ситуация характерна для большинства приложений MVC. Однако при наличии действий, которые зависят от других серверов и требуют длительного времени на выполнение, может оказаться, что все рабочие потоки заняты ожиданием завершения работы из-за других систем.

Сервер способен выполнять больше работы (в конце концов, во время ожидания требуется очень немного ресурсов), но поскольку все рабочие потоки связаны, входящие запросы ставятся в очередь. Возникает довольно странная ситуация, когда приложение выглядит зависшим, в то время как сервер в основном простаивает.

В этот момент у вас может возникнуть идея о собственной реализации пула рабочих потоков, которая точно настроена под конкретное приложение. Ни в коем случае не поступайте так! Написание параллельного кода довольно просто. Однако написание работоспособного параллельного кода достаточно сложно, особенно для новичков в параллельном программировании. Рекомендуется придерживаться стандартного пула. Если вы имеете опыт параллельного программирования, то должны знать, что преимущества будут весьма незначительными по сравнению с усилиями по кодированию и тестированию нового пула потоков.

Итак, решение указанной проблемы заключается в применении асинхронного контроллера. Это увеличит общую производительность приложения, но не предоставит каких-либо преимуществ по выполнению асинхронных операций.

Асинхронные контроллеры полезны только для действий, которые связаны с вводом-выводом или сетью, но не тех, которые интенсивно используют центральный процессор. Проблема, которую вы пытаетесь решить с помощью асинхронных контроллеров, связана с несоответствием между моделью пула и типом обрабатываемых запросов. Пул предназначен для гарантирования того, что каждый запрос получает подходящую долю серверных ресурсов, но в итоге получается набор рабочих потоков, которые ничего не делают. Если применять дополнительные фоновые потоки для действий, интенсивно работающих с центральным процессором, то ресурсы сервера будут растрачены на слишком большое количество одновременно поступающих запросов.

В этом разделе предполагается, что вы знакомы с библиотекой параллельных задач (Task Parallel Library - TPL). Сама библиотека описана в разделе Потоки и файлы, также вы можете посмотреть пример создания асинхронной веб-формы на ASP.NET Web Forms.

Подготовка примера

Перед тем, как приступить к объяснению асинхронных контроллеров, мы продемонстрируем пример проблемы, для решения которой они предназначены. В примере ниже приведен код обычного синхронного контроллера по имени RemoteData, который был добавлен в пример проекта:

using System.Web.Mvc;
using ControllerExtensibility.Models;
using System.Threading.Tasks;

namespace ControllerExtensibility.Controllers
{
    public class RemoteDataController : Controller
    {
        public ActionResult Data()
        {
            RemoteService service = new RemoteService();
            string data = service.GetRemoteData();
            return View((object)data);
        }
    }
}

Этот контроллер содержит метод действия по имени Data(), который создает экземпляр класса модели RemoteService и вызывает на нем метод GetRemoteData(). Данный метод является примером действия, отнимающего значительное время, но мало использующего центральный процессор. В примере ниже показан класс RemoteService, который определен в файле RemoteService.cs внутри папки Models:

using System.Threading;
using System.Threading.Tasks;

namespace ControllerExtensibility.Models
{
    public class RemoteService
    {
        public string GetRemoteData()
        {
            Thread.Sleep(2000);
            return "Hello world";
        }
    }
}

На самом деле метод GetRemoteData() здесь сымитирован. В реальности этот метод мог бы извлекать сложные данные через медленное сетевое соединение, но для простоты мы воспользовались методом Thread.Sleep(), чтобы эмулировать двухсекундную задержку. Последним добавлением является новое представление. Для этого создается папка Views/RemoteData, в которую помещается файл представления Data.cshtml с содержимым, показанным в примере:

@model string
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Data</title>
</head>
<body>
    <div>
        Полученные данные: @Model
    </div>
</body>
</html>

После запуска приложения и перехода на URL вида /RemoteData/Data вызывается соответствующий метод действия, создается объект RemoteService и затем вызывается метод GetRemoteData(). Спустя две секунды (имитирующие выполнение реальной операции) возвращаются данные из GetRemoteData(), которые передаются представлению для визуализации:

Выполнение синхронного контроллера

Проблема заключается в том, что рабочий поток, обрабатывающий запрос, простаивал две секунды - он не делал ничего полезного и не был доступен для обработки других запросов, пока находился в состоянии ожидания.

Создание асинхронного контроллера

Благодаря использованию асинхронного контроллера, рабочий поток освобождается и может обрабатывать другие запросы. Он не предотвращает взаимодействие пользователя с интерфейсом приложения на протяжении двухсекундного ожидания. В конце концов, фиктивные данные будут получены и обработаны. Существуют технологии клиентской стороны (AJAX), с помощью которых можно делать такие запросы в браузере, что позволяет, как минимум, информировать пользователя о ходе получения данных и дать ему возможность продолжить работу с другой частью приложения.

После иллюстрации проблемы, которую мы собираемся решить, давайте перейдем к созданию асинхронного контроллера. Такой контроллер можно создать двумя способами. Первый из них - реализация интерфейса IAsyncController находящегося в пространстве имен System.Web.Mvc.Async, который является асинхронным эквивалентом IController. Этот подход демонстрироваться не будет, т.к. он требует подробного объяснения концепций параллельного программирования .NET.

Внимание по-прежнему будет сосредоточено на MVC Framework, поэтому далее приводится пример второго подхода: применение новых ключевых слов await и async в обычном контроллере.

В предшествующих версиях .NET Framework создание асинхронных контроллеров было трудоемкой задачей и требовало наследования контроллера от специального класса и разделения каждого действия на два метода. Новые ключевые слова await и async существенно упрощают этот процесс: необходимо создать новый объект Task и применить await к результату, как показано в примере ниже:

using System.Web.Mvc;
using ControllerExtensibility.Models;
using System.Threading.Tasks;

namespace ControllerExtensibility.Controllers
{
    public class RemoteDataController : Controller
    {
        public async Task<ActionResult> Data()
        {
            string data = await Task<string>.Factory.StartNew(() =>
            {
                return new RemoteService().GetRemoteData();
            });

            return View((object)data);
        }
    }
}

Старый способ создания асинхронных методов действий все еще поддерживается, хотя рассматриваемый здесь подход намного более элегантен, поэтому рекомендуется использовать именно его. Один из артефактов старого подхода заключается в невозможности применения для методов действий имен, которые заканчиваются на Async (например, IndexAsync()) или Completed (скажем, IndexCompleted()).

Итак, в этом примере был проведен рефакторинг метода действия, так что он теперь возвращает Task<ActionResult>, применены ключевые слова async и await, а также создан объект Task<string>, который отвечает за вызов метода GetRemoteData().

Использование асинхронных методов в контроллере

Асинхронный контроллер можно также применять для использования асинхронных методов в другом месте приложения. В целях демонстрации в класс RemoteService добавлен асинхронный метод, как показано в примере ниже:

using System.Threading;
using System.Threading.Tasks;

namespace ControllerExtensibility.Models
{
    public class RemoteService
    {
        public string GetRemoteData()
        {
            Thread.Sleep(2000);
            return "Hello world";
        }

        public async Task<string> GetRemoteDataAsync()
        {
            return await Task<string>.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                return "Hello world";
            });
        }
    }
}

Результатом метода GetRemoteDataAsync() является объект Task<string>, который выдает то же самое сообщение, что и синхронный метод, когда он завершен. В примере ниже можно видеть, как этот асинхронный метод используется в новом методе действия, добавленном в контроллер RemoteData:

using System.Web.Mvc;
using ControllerExtensibility.Models;
using System.Threading.Tasks;

namespace ControllerExtensibility.Controllers
{
    public class RemoteDataController : Controller
    {
        public async Task<ActionResult> Data()
        {
            string data = await Task<string>.Factory.StartNew(() =>
            {
                return new RemoteService().GetRemoteData();
            });

            return View((object)data);
        }

        public async Task<ActionResult> ConsumeAsyncMethod()
        {
            string data = await new RemoteService().GetRemoteDataAsync();
            return View("Data", (object)data);
        }
    }
}

Как видите, оба метода действий следуют одному и тому же базовому шаблону, а отличие появляется лишь там, где создается объект Task. В результате вызова любого метода действия рабочий поток не будет связан во время ожидания завершения вызова GetRemoteData(). Это означает, что поток доступен для обработки других запросов, тем самым значительно улучшая показатели производительности приложения MVC Framework.

Пройди тесты
Лучший чат для C# программистов