Запуск и забытие асинхронной задачи в 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. У меня есть исходный код в моем блоге, который вы можете добавить в свое решение для этого. Но, пожалуйста, не просто захватите код; пожалуйста, прочитайте весь пост, чтобы было ясно, почему это не лучшее решение.:)

Ответ оказывается немного сложнее:

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

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

  1. Огонь и метод забывания заканчиваются.
  2. Так как он был запущен из асинхронной Задачи, он попытается вернуться в контекст этой Задачи ("маршал") при возврате.
  3. Если асинхронное действие контроллера выполнено и экземпляр контроллера с тех пор был собран сборщиком мусора, контекст этой задачи теперь будет нулевым.

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

  1. А NullReferenceException - очень расплывчато.
  2. Брошенный внутрь кода.Net Framework, вы даже не можете войти внутрь Visual Studio - обычно System.Web.dll.
  3. Не фиксируется никакими 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. Если бы мы ожидали этого, он попытался бы привязать его к этому контексту, который нам не нужен - поэтому мы его не ждем, и это вызывает у нас неприятное предупреждение. Мы отключаем предупреждение, отключив прагма-предупреждение.

Другие вопросы по тегам