При создании веб-приложений часто может возникнуть проблема с обработкой данных, которая отнимает большие ресурсы и машинное время. Механизм ASP.NET имеет ограниченное количество потоков, которые предназначены для обработки пользовательских запросов, полученных от сервера IIS. Проблема состоит в том, что если один из запросов предполагает продолжительную работу с привлечением больших ресурсов, то такой запрос может уменьшить пропускную способность сайта. В случаях, когда таких запросов много, их выполнение может вообще заблокировать доступ пользователей к ресурсу, т. к. все рабочие потоки ASP.NET будут заняты, простаивая в ожидании того, когда завершится выполнение тяжелого запроса к базе данных или сложное вычисление.
Выходом из такой ситуации может служить асинхронное выполнение запросов. При асинхронном выполнении тяжелая задача поручается для выполнения отдельному специально созданному потоку, а основной поток ASP.NET освобождается для обработки других пользовательских запросов. Для реализации такого функционала разработчиками MVC Framework был создан специальный механизм AsyncController, который хоть и не вошел в MVC Framework, но доступен в особой библиотеке MVC Framework Futures, которая представлена файлом Microsoft.Web.Mvc.dll.
Саму библиотеку и документацию к ней на английском языке можно скачать с официальной страницы ASP.NET MVC на сайте Codeplex по следующему адресу: http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471
После добавления ссылки на сборку в проект, для того чтобы использовать асинхронные контроллеры, необходимо проделать некоторые изменения в существующем коде. Первым делом нужно изменить регистрации маршрутов в таблице маршрутизации так, как показано в следующем фрагменте:
routes.MapAsyncRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
Обратите внимание на то, что вызов routes.MapRoute заменен на routes.MapAsyncRoute, это необходимо, чтобы механизм MVC мог обрабатывать как асинхронные, так и синхронные контроллеры. После изменений в регистрации маршрутов нет нужды в других изменениях, чтобы специально отслеживать синхронные контроллеры, поскольку механизм MapAsyncRoute регистрирует маршруты как для асинхронных, так и синхронных контроллеров.
После изменения регистрации маршрутов следует изменить обработчики для *.mvc, определенные ранее в web.config, следующим образом в разделах httpHandlers и handlers:
<add verb="*" path="*.mvc" validate="false"
type="System.Web.Mvc.MvcHttpHandler,
System.Web.Mvc, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add name="MvcHttpHandler" preCondition="integratedMode"
verb="*" path="*.mvc" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
Эти определения обработчиков необходимо заменить на следующие:
<add verb="*" path="*.mvc" validate="false"
type="Microsoft.Web.Mvc.MvcHttpAsyncHandler, Microsoft.Web.Mvc"/>
<add name="MvcHttpHandler" preCondition="integratedMode"
verb="*" path="*.mvc" type="Microsoft.Web.Mvc.MvcHttpAsyncHandler,
Microsoft.Web.Mvc"/>
После всех изменений можно приступать к работе с асинхронными контроллерами. Для того чтобы класс контроллера стал асинхронным, необходимо наследовать его от класса AsyncController:
public class SomeAsyncController : AsyncController
{
...
}
После этого конструктор по умолчанию, унаследованный от AsyncController, определит новый вариант ActionInvoker в виде экземпляра класса AsyncControllerActionInvoker для того, чтобы выполнять асинхронные действия. Для реализации асинхронных действий механизм AsyncController предлагает три паттерна, которые вы вольны использовать по отдельности либо смешивать их друг с другом: IAsyncResult, Event, Delegate.
Паттерн IAsyncResult предполагает, что разработчик сам создаст асинхронную операцию. Согласно этому паттерну, вместо одного метода действия с именем XXX создаются два метода, BeginXXX и EndXXX, со следующим определением параметров:
public IAsyncResult BeginXXX(Guid? userId, AsyncCallback callback, object state);
public ActionResult EndXXX(IAsyncResult asyncResult);
Этот паттерн работает следующим образом:
1. MVC принимает запрос на выполнение действия xxx.
2. Механизмы MVC и AsyncController вызовут BeginXXX точно так же, как и любое другое синхронное действие.
3. Предполагается, что метод Beginxxx создаст некую тяжеловесную асинхронную операцию, например файловое чтение или запрос к базе данных, и использует переданную через параметры функцию обратного вызова callback для вызова после завершения асинхронной операции.
4. После выполнения асинхронной операции будет вызван второй метод Endxxx, которому будет передан результат выполнения Beginxxx в виде экземпляра IAsyncResult.
5. Метод Beginxxx, используя данные, полученные от Beginxxx, формирует обычный для всех действий результат в виде ActionResult или его производных.
Согласно этому паттерну, метод действия разделяется на два метода: запуска и завершения:
public void XXX(Guid? userId);
public ActionResult XXXCompleted(...);
Метод xxx соответствует обычному синхронному действию и вызывается стандартно. Полный механизм работы данного паттерна состоит из следующих действий:
1. MVC принимает запрос на выполнение действия xxx.
2. Механизмы MVC и AsyncController вызовут XXX точно так же, как и любое другое синхронное действие.
3. Разработчик определяет внутри метода xxx асинхронную операцию, после запуска которой метод завершает свое выполнение.
4. Чтобы механизм асинхронных контроллеров мог определить, когда следует вызвать XXXCompleted, разработчик должен воспользоваться свойством AsyncManager.OutstandingOperations, которое является стандартным для класса контроллера AsyncController.
5. Разработчик инкрементирует AsyncManager.OutstandingOperations при создании каждого асинхронного процесса и заботится о том, чтобы по завершению процесса свойство AsyncManager.OutstandingOperations было декрементировано.
6. Механизм AcyncControllerActionInvoker следит за свойством AsyncManager.OutstandingOperations и вызывает XXXCompleted, когда это свойство обнулится, что означает завершение работы всех асинхронных процессов.
7. Параметры для XXXCompleted определяет разработчик. Для того чтобы AcyncControllerActionInvoker мог правильно выполнить XXXCompleted и передать необходимые параметры, разработчик заполняет специальную структуру AsyncManager.Parameters, которая является частью класса AsyncController. Структура AsyncManager.Parameters заполняется при работе метода xxx.
8. После завершения работы XXX механизм AcyncControllerActionInvoker вызывает метод XXXCompleted с набором параметров на базе AsyncManager.Parameters.
9. Используя переданные параметры, метод XXXCompleted возвращает стандартный результат в виде ActionResult или его производных.
Следующий фрагмент кода демонстрирует реализацию паттерна Event:
public void SelectUser(Guid? userId)
{
AsyncManager.Parameters["userData''] = new UserData();
AsyncManager.OutstandingOperations.Increment();
ThreadPool.QueueUserWorkItem(o =>
{
Thread.Sleep(2000);
AsyncManager.OutstandingOperations.Decrement();
}, null);
}
public ActionResult SelectUserCompleted(UserData userData)
{
...
}
Чтобы упростить процесс, существует альтернатива прямому инкрементированию и декрементированию. С помощью стандартной части AsyncController метода AsyncManager.RegisterTask можно использовать связку из паттернов IAsyncResult и Event. Рассмотрим на примере:
public void XXX(Guid? userId)
{
AsyncManager.RegisterTask(
callback => BeginXXX(userId, callback, null),
asyncResult =>
{
UserData userData = EndXXX(asyncResult);
AsyncManager.Parameters["userData"] = userData;
}
);
}
public ActionResult XXXCompleted(UserData userData)
{
// ...
}
Во фрагменте кода используется паттерн Event, согласно которому создается два метода: xxx и xxxCompleted. Метод xxx регистрирует асинхронную задачу с помощью механизма AsyncManager.RegisterTask, который принимает два параметра: анонимные функции, осуществляющие логику паттерна IAsyncResult. Первая функция вызывает метод Beginxxx, который выполняет некую асинхронную операцию. Вторая анонимная функция выполняется тогда, когда асинхронная операция заканчивается и ей передаются результаты вызова Beginxxx. Задача второй анонимной функции состоит в том, чтобы, используя метод Endxxx паттерна IAsyncResult, получить значения параметров для метода XXXCompleted.
Преимущество данной связки паттернов состоит в том, что разработчику предоставляется возможность создавать несколько следующих подряд вызовов механизма AsyncManager.RegisterTask, которые выполняют разные асинхронные операции и формируют результаты разных параметров для метода XXXCompleted. Так как механизм AsyncManager.RegisterTask самостоятельно отслеживает инкремент и декремент свойства AsyncManager.OutstandingOperations, разработчику предлагается более безопасный и простой механизм создания нескольких асинхронных операций в одном запросе.
Этот паттерн похож на паттерн Event с одним существенным отличием: отсутствует метод xxxComplete. Вместо этого метод xxx сам занимается возвращением результата ActionResult на основании данных, полученных от асинхронных операций. Так выглядит определение метода действия при использовании паттерна Delegate:
public Func<ActionResult> Foo(Guid? userId)
Для демонстрации реализации данного паттерна перепишем пример паттерна Event по-другому:
public Func<ActionResult> XXX(Guid userId)
{
UserData userData = new UserData();
AsyncManager.RegisterTask(
callback => BeginXXX(userId, callback, null),
asyncResult =>
{
userData = EndXXX(asyncResult);
}
);
return () => {
ViewData["userData"] = userData;
return View() ;
};
}
Главное отличие реализации паттерна Delegate в приведенном фрагменте от паттерна Event состоит в том, что для возвращения результата выполнения действия используется не ActionResult, а Func<ActionResult>, который представляет собой анонимную функцию, возвращающую результат в виде ActionResult. По сравнению с паттерном Event данный паттерн имеет упрощенный единый механизм, не разделенный на несколько методов, и максимально напоминает работу действий в синхронных контроллерах. При использовании этого паттерна у разработчика нет необходимости заботиться ни об обработке AsyncManager.OutstandingOperations, ни о заполнении AsyncManager.Parameters.
Для асинхронных операций важно понятие времени исполнения запроса, поэтому в стандартный механизм класса AsyncController входит свойство AsyncManager.Timeout, которое позволяет задавать максимальное время ожидания результата выполнения асинхронного действия. В случае, когда действие выполняется дольше, чем определено в AsyncManager.Timeout механизмом, будет вызвано исключение TimeoutException. Для более гибкого управления максимальным периодом ожидания ответа от действия механизм асинхронных контроллеров предлагает два атрибута: AsyncTimeoutAttribute и NoAsyncTimeoutAttribute. Первый устанавливает время ожидания для конкретного действия или контроллера, второй указывает, что ожидания ответа не должно вызвать исключения и ожидать ответа от асинхронного действия требуется без ограничения по времени.
Одним из ограничений механизма асинхронных контроллеров является ограничение на именование методов действий. Вы не можете называть методы действий с префиксами Begin, End и суффиксом Completed. Это ограничение призвано предотвратить прямой вызов методов типа BeginXXX, EndXXX или XXXCompleted вместо вызова XXX. Тем не менее вы можете воспользоваться атрибутом ActionNameAttribute для того, чтобы задать необходимый псевдоним методу действия. Следующий фрагмент демонстрирует это:
[ActionName("BeginProcess")]
public ActionResult DoProcess();
Еще одним требованием механизма асинхронных контроллеров является исключение Default.aspx из корня проекта в случае, когда запросы к корневому ресурсу будут асинхронными. Default.aspx, включенный в стандартный проект MVC Framework, может работать только с синхронными запросами.