Запуск и забытие асинхронной задачи в MVC Action
У меня есть стандартное, не асинхронное действие, как:
[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
PdfGenerator.Current.GenerateAsync(id);
return Json(null);
}
Идея в том, что я знаю, что это создание PDF может занять много времени, поэтому я просто запускаю задачу и возвращаюсь, не заботясь о результате асинхронной операции.
В приложении ASP.Net MVC 4 по умолчанию это дает мне приятное исключение:
System.InvalidOperationException: асинхронная операция не может быть начата в это время. Асинхронные операции могут быть запущены только внутри асинхронного обработчика или модуля или во время определенных событий в жизненном цикле страницы. Если это исключение произошло во время выполнения страницы, убедитесь, что страница помечена как <% @ Page Async = "true"%>.
Что все виды не имеют отношения к моему сценарию. Глядя на это, я могу установить флаг в false, чтобы предотвратить это исключение:
<appSettings>
<!-- Allows throwaway async operations from MVC Controller actions -->
<add key="aspnet:AllowAsyncDuringSyncStages" value="true" />
</appSettings>
/questions/23337442/mvcmailer-sendasync-ne-rabotaet-s-amazon-ses/23337453#23337453
http://msdn.microsoft.com/en-us/library/hh975440.aspx
Но вопрос в том, есть ли какой-нибудь вред, если запустить эту асинхронную операцию и забыть об этом из синхронного действия контроллера MVC? Все, что я могу найти, рекомендует сделать Controller Async, но это не то, что я ищу - не было бы никакого смысла, так как он всегда должен возвращаться немедленно.
3 ответа
Расслабьтесь, как говорит сама Microsoft ( http://msdn.microsoft.com/en-us/library/system.web.httpcontext.allowasyncduringsyncstages.aspx):
Это поведение предназначено для обеспечения безопасности, чтобы на раннем этапе вы знали, пишете ли вы асинхронный код, который не соответствует ожидаемым шаблонам и может иметь отрицательные побочные эффекты.
Просто запомните несколько простых правил:
Никогда не ждите внутри (асинхронных или нет) пустых событий (так как они возвращаются немедленно). Некоторые события страницы WebForms поддерживают простое ожидание внутри них - но
RegisterAsyncTask
все еще очень предпочтительный подход.Не ждите от асинхронных пустых методов (так как они возвращаются немедленно).
Не ждите синхронно в GUI или потоке запросов (
.Wait()
,.Result()
,.WaitAll()
,WaitAny()
) на асинхронных методах, которые не имеют.ConfigureAwait(false)
под корнем ждут внутри них, или их кореньTask
не начинается с.Run()
или не имеютTaskScheduler.Default
явно указано (как GUI или запрос, таким образом, тупик).использование
.ConfigureAwait(false)
или жеTask.Run
или явно указатьTaskScheduler.Default
для каждого фонового процесса и в каждом библиотечном методе, который не нуждается в продолжении контекста синхронизации - думайте о нем как о "вызывающем потоке", но знайте, что он не один (и не всегда в одном и том же), и может даже больше не существовать (если Запрос уже завершен). Одно это позволяет избежать наиболее распространенных ошибок асинхронного ожидания и ожидания, а также повышает производительность.
Microsoft просто предположила, что вы забыли подождать с вашей задачей...
ОБНОВЛЕНИЕ: Как ясно сказал Стивен (каламбур не предназначен) в своем ответе, существует наследственная, но скрытая опасность со всеми формами "забей и забудь" при работе с пулами приложений, не только специфичными только для async/await, но и для Tasks, ThreadPool и все другие подобные методы - они не гарантированно завершатся после завершения запроса (пул приложений может перезапускаться в любое время по ряду причин).
Вы можете заботиться об этом или нет (если это не критично для бизнеса, как в конкретном случае ОП), но вы всегда должны знать об этом.
InvalidOperationException
это не предупреждение. AllowAsyncDuringSyncStages
это опасная обстановка, которую я бы никогда не использовал.
Правильное решение - сохранить запрос в постоянной очереди (например, очереди Azure) и иметь отдельное приложение (например, рабочую роль Azure), обрабатывающее эту очередь. Это гораздо больше работы, но это правильный способ сделать это. Я имею в виду "правильный" в том смысле, что IIS/ASP.NET, перерабатывающий ваше приложение, не испортит вашу обработку.
Если вы абсолютно хотите сохранить свою обработку в памяти (и, как следствие, все в порядке с "проигрышными" запросами), то, по крайней мере, зарегистрируйте работу в ASP.NET. У меня есть исходный код в моем блоге, который вы можете добавить в свое решение для этого. Но, пожалуйста, не просто захватите код; пожалуйста, прочитайте весь пост, чтобы было ясно, почему это не лучшее решение.:)
Ответ оказывается немного сложнее:
Если то, что вы делаете, как в моем примере, это просто установка длительной асинхронной задачи и возврат, вам не нужно делать больше, чем я указал в своем вопросе.
Но есть риск: если кто-то расширит это Действие позже, когда будет иметь смысл асинхронное Действие, то метод асинхронного запуска и забывания внутри него будет случайным образом завершаться или завершаться неудачей. Это выглядит так:
- Огонь и метод забывания заканчиваются.
- Так как он был запущен из асинхронной Задачи, он попытается вернуться в контекст этой Задачи ("маршал") при возврате.
- Если асинхронное действие контроллера выполнено и экземпляр контроллера с тех пор был собран сборщиком мусора, контекст этой задачи теперь будет нулевым.
Будет ли оно на самом деле нулевым, будет меняться из-за вышеупомянутых моментов времени - иногда это так, иногда это не так. Это означает, что разработчик может проверить и найти все, что работает правильно, перейти к производству, и он взрывается. Хуже, ошибка, которую это вызывает:
- А NullReferenceException - очень расплывчато.
- Брошенный внутрь кода.Net Framework, вы даже не можете войти внутрь Visual Studio - обычно System.Web.dll.
- Не фиксируется никакими try/catch, потому что та часть параллельной библиотеки задач, которая позволяет вам вернуться в существующие контексты try/catch, является ошибочной частью.
Таким образом, вы получите загадочную ошибку, когда вещи просто не происходят - исключения генерируются, но вы, вероятно, не знаете их. Нехорошо.
Чистый способ предотвратить это:
[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
#pragma warning disable 4014 // Fire and forget.
Task.Run(async () =>
{
await PdfGenerator.Current.GenerateAsync(id);
}).ConfigureAwait(false);
return Json(null);
}
Итак, здесь у нас есть синхронный контроллер без проблем - но чтобы гарантировать, что он все еще не будет работать, даже если мы позже изменим его на асинхронный, мы явно запустим новую задачу через Run, которая по умолчанию помещает задачу в основной ThreadPool. Если бы мы ожидали этого, он попытался бы привязать его к этому контексту, который нам не нужен - поэтому мы его не ждем, и это вызывает у нас неприятное предупреждение. Мы отключаем предупреждение, отключив прагма-предупреждение.