Главная страница   /   17.6. Улучшение работы приложения при помощи специальных контроллеров (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

17.6. Улучшение работы приложения при помощи специальных контроллеров

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

Используем контроллеры без поддержки состояния сессии (sessionless controllers)

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

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

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

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

Как уже упоминалось в начале этой главы, интерфейс IControllerFactory содержит метод GetControllerSessionBehaviour, который возвращает значение из перечисления SessionStateBehaviour. Это перечисление содержит четыре значения, которые определяют конфигурацию состояния сессии контроллера. Они описаны в таблице 17-3.

Таблица 17-3: Значения перечисления SessionStateBehavior
Значение Описание
Default Использует стандартное поведение ASP.NET, то есть определяет конфигурацию состояния сессии из HttpContext.
Required Разрешает чтение и запись состояния сессии.
ReadOnly Разрешает только чтение состояния сессии.
Disabled Состояние сессии отключено.

Фабрика контроллеров, которая реализует интерфейс IControllerFactory, напрямую устанавливает поведение состояния сессии для контроллеров, возвращая значения SessionStateBehavior из метода GetControllerSessionBehavior. Параметрами этого метода являются объект RequestContext и string, содержащий имя контроллера. Вы можете вернуть любое из четырех значений, приведенных в таблице, причем для разных контроллеров можно возвращать разные значения. Мы изменили реализацию метода GetControllerSessionBehavior в классе CustomControllerFactory, который создали ранее в данной главе, как показано в листинге 17-23.

Листинг 17-23: Определяем поведение состояния сессии для контроллера
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 к отдельным классам контроллеров, как показано в листинге 17-24 на примере нового контроллера FastController.

Листинг 17-24: Используем атрибут SessionState
using System.Web.Mvc;
using System.Web.SessionState;
using ControllerExtensibility.Models;
namespace ControllerExtensibility.Controllers
{
	[SessionState(SessionStateBehavior.Disabled)]
	public class FastController : Controller
	{
		public ActionResult Index()
		{
			return View("Result", new Result
			{
				ControllerName = "Fast ",
				ActionName = "Index"
			});
		}
	}
}

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

Session["Message"] = "Hello";

или прочитать информацию из состояния сессии в представлении:

Message: @Session["Message"]

MVC Framework выбросит исключение при вызове действия или визуализации представления.

Подсказка

Когда состояние сеанса отключено, свойство HttpContext.Session возвращает значение null.

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

Подсказка

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

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

Базовая платформа ASP.NET поддерживает пул потоков .NET, которые используются для обработки клиентских запросов. Этот пул называется пулом рабочих потоков (worker thread pool), а потоки, соответственно, рабочими (worker threads). При получении запроса рабочий поток вызывается из пула и обрабатывает запрос.

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

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

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

Примечание

В этом разделе мы предполагаем, что вы знакомы с Task Parallel Library (TPL). Если вы хотите узнать больше о TPL, прочтите книгу Адама на эту тему, Pro .NET Parallel Programming in C# издательства Apress.

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

Внимание!

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

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

Примечание

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

Создаем пример

Чтобы начать изучение асинхронных контроллеров, мы продемонстрируем вам проблему, которую они могут решить. В листинге 17-25 показан обычный синхронный контроллер под названием RemoteData, который мы добавили в проект.

Листинг 17-25: Проблемный синхронный контроллер
using System.Web.Mvc;
using ControllerExtensibility.Models;
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 показан в листинге 17-26.

Листинг 17-26: Сущность модели с помощью трудоемким методом
using System.Threading;
namespace ControllerExtensibility.Models
{
	public class RemoteService
	{
		public string GetRemoteData()
		{
			Thread.Sleep(2000);
			return "Hello from the other side of the world";
		}
	}
}

Okay, здесь мы просто подделали метод GetRemoteData. В реальном мире этот метод мог бы извлекать сложные данные через медленное сетевое подключение, но для простоты мы использовали метод Thread.Sleep для моделирования двух-секундной задержки. Мы также создали простое представление под названием Data.cshtml, которое показано в листинге 17-27.

Листинг 17-27: Представление Data
@model string
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Data</title>
</head>
<body>
	<div>
		Data: @Model
	</div>
</body>
</html>

Если вы запустите приложение и перейдите по ссылке RemoteData/Data, будет вызван метод действия, создан объект RemoteService и отправлен вызов к методу GetRemoteData. Через две секунды (которые имитируют выполнение реальной операции) метод GetRemoteData возвращает данные, они передаются представлению и визуализируются, как показано на рисунке 17-12.

Рисунок 17-12: Переход по ссылке /RemoteData/Data

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

Создаем асинхронный контроллер

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

Подсказка

Не все действия в асинхронных контроллерах должны быть асинхронными. Вы можете включать в них и синхронные методы, и они будут работать нормально.

Мы хотим, чтобы в центре нашего внимания оставался MVC Framework, и поэтому продемонстрируем второй подход: наследовать класс контроллера от System.Web.Mvc.AsyncController, который реализует IAsyncController. В предыдущей версии .NET Framework создание асинхронных контроллеров было сложным процессом и требовало разделения действия на два метода. Новые ключевые слова await и async, о которых мы рассказывали в главе 4, намного упростили этот процесс: вы создаете новый объект Task и ждете его ответа, как показано в листинге 17-28.

Подсказка

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

Листинг 17-28: Создаем асинхронный контроллер без изменения вызываемых им методов
using System.Web.Mvc;
using ControllerExtensibility.Models;
using System.Threading.Tasks;

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

Мы выделили изменения, которые делают контроллер RemoteData асинхронным. Кроме изменения базового класса AsyncController, мы провели рефакторинг метода действия так, чтобы он возвращал Task<ActionResult>, применили ключевые слова async и await и создали Task<string>, который будет вызывать метод GetRemoteData.

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

С помощью асинхронных контроллеров можно использовать асинхронные методы где угодно в приложении. Чтобы это продемонстрировать, мы добавили асинхронный метод в класс RemoteService, как показано в листинге 17-29.

Листинг 17-29: Добавляем асинхронный метод в класс RemoteService
using System.Threading;
using System.Threading.Tasks;

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

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

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

Листинг 17-30: Используем асинхронный метод в контроллере RemoteData
using System.Web.Mvc;
using ControllerExtensibility.Models;
using System.Threading.Tasks;

namespace ControllerExtensibility.Controllers
{
	public class RemoteDataController : AsyncController
	{
		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. Это означает, что поток сможет обрабатывать другие запросы, что может значительно улучшить производительность приложения.